Skip to main content

Overview

EPR LAPS Backend uses mongo-locks for distributed locking to prevent race conditions in concurrent write operations across multiple service instances.
Always use locks for critical sections that modify shared resources to prevent data corruption in distributed environments.

Why Use Locks?

Distributed locks are essential when:
  • Multiple service instances run concurrently
  • Operations modify shared data in MongoDB
  • Race conditions could cause data inconsistency
  • Operations must be atomic across multiple documents

Basic Usage

Accessing the Locker

The locker is available via server.locker or request.locker:
// In server methods
async function doStuff(server) {
  const lock = await server.locker.lock('unique-resource-name')
  // ...
}

// In route handlers
const handler = async (request, h) => {
  const lock = await request.locker.lock('unique-resource-name')
  // ...
}

Traditional Lock Pattern

The recommended pattern for using locks:
async function doStuff(server) {
  const lock = await server.locker.lock('unique-resource-name')

  if (!lock) {
    // Lock unavailable - another process holds it
    return
  }

  try {
    // Perform critical operations here
    // These operations are now protected from concurrent access
  } finally {
    // Always release the lock
    await lock.free()
  }
}
Key Points:
  • Always check if lock acquisition succeeded (if (!lock))
  • Use try/finally to ensure lock is released even if errors occur
  • Keep the locked section small and atomic

Using await using (ES2022)

Modern JavaScript supports automatic resource management with await using, which automatically releases the lock.
async function doStuff(server) {
  await using lock = await server.locker.lock('unique-resource-name')

  if (!lock) {
    // Lock unavailable
    return
  }

  // Perform critical operations
  // Lock is automatically released when lock goes out of scope
}
Caveat: Test coverage reports may not correctly handle await using syntax.

Helper Functions

The application provides helper methods in src/common/helpers/mongo-lock.js:

acquireLock()

Attempts to acquire a lock with optional logging:
import { acquireLock } from './common/helpers/mongo-lock.js'

async function processPayment(server, logger, paymentId) {
  const lock = await acquireLock(
    server.locker,
    `payment-${paymentId}`,
    logger
  )

  if (!lock) {
    // Lock acquisition failed (logged automatically)
    return { success: false, error: 'Resource locked' }
  }

  try {
    // Process payment
    return { success: true }
  } finally {
    await lock.free()
  }
}
Function signature:
async function acquireLock(locker, resource, logger)
Parameters:
  • locker - The locker instance (server.locker or request.locker)
  • resource - Unique resource identifier string
  • logger - Optional logger instance for error logging
Returns: Lock object or null if unavailable

requireLock()

Requires a lock to be acquired, throwing an error if unavailable:
import { requireLock } from './common/helpers/mongo-lock.js'

async function criticalOperation(server, resourceId) {
  const lock = await requireLock(
    server.locker,
    `critical-${resourceId}`
  )

  try {
    // Lock is guaranteed to be acquired
    // Perform critical operation
  } finally {
    await lock.free()
  }
}
Function signature:
async function requireLock(locker, resource)
Parameters:
  • locker - The locker instance
  • resource - Unique resource identifier string
Returns: Lock object Throws: Error if lock cannot be acquired
requireLock() will throw an exception if the lock is unavailable. Use acquireLock() for graceful handling.

Helper Implementation

From src/common/helpers/mongo-lock.js:
async function acquireLock(locker, resource, logger) {
  const lock = await locker.lock(resource)
  if (!lock) {
    if (logger) {
      logger.error(`Failed to acquire lock for ${resource}`)
    }
    return null
  }
  return lock
}

async function requireLock(locker, resource) {
  const lock = await locker.lock(resource)
  if (!lock) {
    throw new Error(`Failed to acquire lock for ${resource}`)
  }
  return lock
}

Best Practices

1

Use descriptive resource names

Choose clear, unique identifiers for lock resources:
// Good
const lock = await server.locker.lock(`bank-details-${organisationId}`)
const lock = await server.locker.lock(`payment-processing-${paymentId}`)

// Bad
const lock = await server.locker.lock('lock1')
const lock = await server.locker.lock('data')
2

Keep critical sections small

Minimize the time a lock is held:
// Good - lock only during write
const data = await fetchData() // No lock needed
const processed = processData(data) // No lock needed

const lock = await server.locker.lock('resource')
try {
  await saveData(processed) // Lock held briefly
} finally {
  await lock.free()
}

// Bad - lock held unnecessarily long
const lock = await server.locker.lock('resource')
try {
  const data = await fetchData() // Slow operation
  const processed = processData(data) // CPU-intensive
  await saveData(processed)
} finally {
  await lock.free()
}
3

Always release locks

Use try/finally or await using to guarantee lock release:
const lock = await server.locker.lock('resource')
if (!lock) return

try {
  await operation()
} finally {
  await lock.free() // Always executes
}
4

Handle lock unavailability gracefully

Always check if lock acquisition succeeded:
const lock = await server.locker.lock('resource')

if (!lock) {
  // Handle gracefully - don't proceed with operation
  logger.warn('Resource is locked, skipping operation')
  return { status: 'locked', retry: true }
}

Common Patterns

Pattern 1: Single Resource Lock

async function updateBankDetails(server, organisationId, newDetails) {
  const lock = await server.locker.lock(`bank-details-${organisationId}`)
  
  if (!lock) {
    throw new Error('Bank details are currently being updated')
  }
  
  try {
    const existing = await server.db.collection('bank_details')
      .findOne({ organisationId })
    
    await server.db.collection('bank_details')
      .updateOne(
        { organisationId },
        { $set: newDetails }
      )
    
    return { success: true }
  } finally {
    await lock.free()
  }
}

Pattern 2: Multiple Resources

When locking multiple resources, always acquire locks in the same order to prevent deadlocks.
async function transferFunds(server, fromAccount, toAccount, amount) {
  // Sort to ensure consistent lock order
  const [first, second] = [fromAccount, toAccount].sort()
  
  const lock1 = await server.locker.lock(`account-${first}`)
  if (!lock1) return { error: 'Account locked' }
  
  try {
    const lock2 = await server.locker.lock(`account-${second}`)
    if (!lock2) return { error: 'Account locked' }
    
    try {
      // Perform transfer
      await debit(fromAccount, amount)
      await credit(toAccount, amount)
      return { success: true }
    } finally {
      await lock2.free()
    }
  } finally {
    await lock1.free()
  }
}

Pattern 3: Retry with Backoff

async function updateWithRetry(server, resourceId, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const lock = await server.locker.lock(`resource-${resourceId}`)
    
    if (lock) {
      try {
        await performUpdate(resourceId)
        return { success: true }
      } finally {
        await lock.free()
      }
    }
    
    // Wait before retry with exponential backoff
    const delay = Math.pow(2, attempt) * 100
    await new Promise(resolve => setTimeout(resolve, delay))
  }
  
  return { error: 'Failed to acquire lock after retries' }
}

Testing with Locks

Mock the Locker

import { describe, it, expect, vi } from 'vitest'

describe('Lock usage', () => {
  it('should acquire and release lock', async () => {
    const mockLock = {
      free: vi.fn().mockResolvedValue(undefined)
    }
    
    const server = {
      locker: {
        lock: vi.fn().mockResolvedValue(mockLock)
      }
    }
    
    await myFunction(server)
    
    expect(server.locker.lock).toHaveBeenCalledWith('resource-name')
    expect(mockLock.free).toHaveBeenCalled()
  })
})

Lock Configuration

The mongo-locks library uses the MongoDB connection configured in the application. No additional configuration is required beyond the MongoDB settings.

Next Steps

Configuration

Configure MongoDB connection settings

Testing

Learn how to test code with locks

Build docs developers (and LLMs) love