UNPKG

s3-mutex

Version:

A robust distributed locking mechanism for Node.js applications using AWS S3 as the backend storage, with support for deadlock detection, timeout handling, automatic lock refresh, retry with backoff, and cleanup utilities.

201 lines (198 loc) 6.77 kB
import { S3Client, S3ClientConfig } from '@aws-sdk/client-s3'; import { Readable } from 'node:stream'; /** * Converts a readable stream to a string */ declare function streamToString(stream: Readable | ReadableStream<Uint8Array> | Blob): Promise<string>; interface S3MutexOptions { /** * AWS S3 client instance */ s3Client?: S3Client; s3ClientConfig?: S3ClientConfig; /** * S3 bucket name where locks will be stored */ bucketName: string; /** * Whether to create the bucket if it doesn't exist * @default false */ createBucketIfNotExists?: boolean; /** * Key prefix for lock files (optional) */ keyPrefix?: string; /** * Max number of retries when acquiring a lock * @default 5 */ maxRetries?: number; /** * Base delay between retries in milliseconds (used for exponential backoff) * @default 200 */ retryDelayMs?: number; /** * Maximum delay between retries in milliseconds (caps the exponential backoff) * @default 5000 (5 seconds) */ maxRetryDelayMs?: number; /** * Whether to add jitter to retry delays to prevent synchronized retries * @default true */ useJitter?: boolean; /** * Lock timeout in milliseconds. After this time, the lock is considered stale * and can be forcefully acquired by another process. * @default 60000 (1 minute) */ lockTimeoutMs?: number; /** * Clock skew tolerance in milliseconds. This value is added to expiration calculations * to account for differences in system clocks between distributed processes. * @default 1000 (1 second) */ clockSkewToleranceMs?: number; } interface LockInfo { locked: boolean; owner?: string; acquiredAt?: number; expiresAt?: number; priority?: number; } declare class S3Mutex { private s3Client; private bucketName; private keyPrefix; private maxRetries; private retryDelayMs; private maxRetryDelayMs; private useJitter; private lockTimeoutMs; private clockSkewToleranceMs; private ownerId; private createBucketIfNotExists; private bucketInitialized; private heldLocks; private lockRequests; private lockDependencies; constructor(options: S3MutexOptions); /** * Ensures the S3 bucket exists, creating it if necessary and configured to do so */ private ensureBucketExists; /** * Implements exponential backoff with optional jitter * @param attempt The current attempt number (0-based) * @returns A promise that resolves after the calculated delay */ private exponentialBackoff; /** * Formats the full S3 key for a lock */ private getLockKey; /** * Utility function to clean an ETag by removing quotes */ private cleanETag; /** * Standard error handling for S3 operations */ private handleS3Error; /** * Atomically initializes a lock if it doesn't exist * Uses a more atomic approach to initialize the lock file */ private initializeLock; /** * Gets the current state of a lock */ private getLockInfo; /** * Updates the lock info in S3 with improved error handling */ private updateLockInfo; /** * Registers a lock dependency to help with deadlock detection * @param waitingOwner The owner waiting for the lock * @param targetLockName The lock being waited for * @param targetOwner The owner currently holding the lock */ private registerLockDependency; /** * Checks if there's a potential deadlock scenario by detecting circular wait conditions * @param lockName The lock we're trying to acquire * @param currentOwner The current owner of the lock * @returns True if there's a potential deadlock */ private isPotentialDeadlock; /** * Attempts to acquire a lock * @param lockName Name of the lock to acquire * @param timeoutMs Maximum time to wait for lock acquisition * @param priority Optional priority for this lock request (higher values have higher priority) * @returns A promise that resolves to true if the lock was acquired, false otherwise */ acquireLock(lockName: string, timeoutMs?: number, priority?: number): Promise<boolean>; /** * Refreshes a lock's expiration time * @param lockName Name of the lock to refresh * @returns A promise that resolves to true if the lock was refreshed, false otherwise */ refreshLock(lockName: string): Promise<boolean>; /** * Releases a lock * @param lockName Name of the lock to release * @param force If true, release the lock even if it's not owned by us * @returns A promise that resolves to true if the lock was released, false otherwise */ releaseLock(lockName: string, force?: boolean): Promise<boolean>; /** * Executes a function while holding a lock * @param lockName Name of the lock to acquire * @param fn Function to execute while holding the lock * @param options Options for lock acquisition * @returns A promise that resolves to the result of the function or null if the lock couldn't be acquired */ withLock<T>(lockName: string, fn: () => Promise<T>, options?: { timeoutMs?: number; retries?: number; }): Promise<T | null>; /** * Check if a lock exists and is currently locked * @param lockName Name of the lock to check * @returns A promise that resolves to true if the lock exists and is locked, false otherwise */ isLocked(lockName: string): Promise<boolean>; /** * Check if we own a specific lock * @param lockName Name of the lock to check * @returns A promise that resolves to true if we own the lock, false otherwise */ isOwnedByUs(lockName: string): Promise<boolean>; /** * Completely removes a lock file from S3 * @param lockName Name of the lock to delete * @param force If true, delete the lock even if it's not owned by us * @returns A promise that resolves to true if the lock was deleted, false otherwise */ deleteLock(lockName: string, force?: boolean): Promise<boolean>; /** * Finds and cleans up stale locks * @param options Optional configuration for cleanup * @returns A promise that resolves to the number of locks cleaned up */ cleanupStaleLocks(options?: { prefix?: string; olderThan?: number; dryRun?: boolean; }): Promise<{ cleaned: number; total: number; stale: number; }>; } export { type LockInfo, S3Mutex, type S3MutexOptions, streamToString };