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.
168 lines (130 loc) • 5.59 kB
Markdown
A distributed locking mechanism for Node.js applications using AWS S3 as the backend storage.
- **Distributed locking**: Coordinate access across multiple services
- **Deadlock detection**: Priority-based mechanism for deadlock resolution
- **Timeout handling**: Automatic lock expiration with configurable timeouts
- **Lock heartbeat**: Automatic lock refresh during long operations
- **Retry with backoff and jitter**: Configurable retry mechanism
- **Error handling**: Specific handling for S3 service issues
- **Cleanup utilities**: Tools for managing stale locks
> **⚠️ Warning**: S3-based locking has significant limitations compared to purpose-built locking solutions. S3 operations have higher latency and are not optimized for high-frequency lock operations. Consider alternatives like Redis, DynamoDB, or ZooKeeper for mission-critical applications.
```bash
npm install s3-mutex
yarn add s3-mutex
pnpm add s3-mutex
```
```typescript
import { S3Client } from "@aws-sdk/client-s3";
import { S3Mutex } from "s3-mutex";
// Initialize S3 client
const s3Client = new S3Client({
region: "us-east-1",
// other configuration options
});
// make sure your bucket exists first...
// Create mutex instance
const mutex = new S3Mutex({
s3Client,
bucketName: "my-locks-bucket",
keyPrefix: "locks/", // optional, defaults to "locks/"
});
// Acquire a lock
const acquired = await mutex.acquireLock("my-resource-lock");
if (acquired) {
try {
// Do work with the exclusive lock
await doSomething();
} finally {
// Release the lock when done
await mutex.releaseLock("my-resource-lock");
}
} else {
console.log("Failed to acquire lock");
}
```
The `withLock` helper method simplifies working with locks by automatically releasing them:
```typescript
// Execute a function with an automatic lock
const result = await mutex.withLock("my-resource-lock", async () => {
// This function is executed only when the lock is acquired
const data = await processResource();
return data;
});
if (result === null) {
// Lock acquisition failed
console.log("Could not acquire lock");
} else {
// Lock was acquired, function executed, and lock released
console.log("Process completed with result:", result);
}
```
```typescript
const mutex = new S3Mutex({
// Required options
s3Client: s3Client,
bucketName: "my-locks-bucket",
// Optional configuration with defaults
keyPrefix: "locks/", // Prefix for lock keys in S3
maxRetries: 5, // Max number of acquisition attempts
retryDelayMs: 200, // Base delay between retries
maxRetryDelayMs: 5000, // Max delay between retries
useJitter: true, // Add randomness to retry delays
lockTimeoutMs: 60000, // Lock expiration (1 minute)
clockSkewToleranceMs: 1000, // Tolerance for clock differences
});
```
```typescript
new S3Mutex(options: S3MutexOptions)
```
### Methods
- **acquireLock(lockName, timeoutMs?, priority?)**: Acquire a named lock with optional timeout and priority
- **releaseLock(lockName, force?)**: Release a lock, with optional force parameter
- **refreshLock(lockName)**: Refresh a lock's expiration time
- **isLocked(lockName)**: Check if a lock is currently held
- **isOwnedByUs(lockName)**: Check if we own a specific lock
- **deleteLock(lockName, force?)**: Completely remove a lock file
- **withLock(lockName, fn, options?)**: Execute a function with an automatic lock
- **cleanupStaleLocks(options?)**: Find and clean up expired locks
### Lock Priority and Deadlock Prevention
S3-Mutex includes deadlock prevention through priority-based acquisition. When multiple processes attempt to acquire locks, those with higher priority values will be favored if deadlock conditions are detected.
```typescript
// Acquire with priority (higher value = higher priority)
await mutex.acquireLock("resource-lock", undefined, 10);
```
```typescript
// Find and clean up stale locks
const results = await mutex.cleanupStaleLocks({
prefix: "locks/myapp/", // Optional prefix to limit cleanup scope
olderThan: Date.now() - 3600000, // Optional custom age (default is lockTimeoutMs)
dryRun: true, // Optional: just report stale locks without deleting
});
console.log(`Found ${results.stale} stale locks out of ${results.total} total locks`);
console.log(`Cleaned up ${results.cleaned} locks`);
```
```typescript
// Force release a lock (use with caution)
await mutex.releaseLock("resource-lock", true);
```
1. **Set appropriate timeouts**: Configure lock timeouts that match your workload duration
2. **Handle failure gracefully**: Always check if lock acquisition was successful
3. **Use the withLock helper**: Ensures locks are always released, even if errors occur
4. **Implement proper error handling**: Be prepared for S3 service errors and throttling
5. **Run periodic cleanup**: Use the cleanupStaleLocks method to maintain your lock storage
6. **Consider performance implications**: S3 operations have higher latency than in-memory solutions
7. **Test thoroughly under load**: Verify lock reliability under your specific workload conditions
8. **Have a fallback strategy**: Plan for occasional lock failures in production environments
9. **Monitor lock contention**: High contention may indicate need for architectural changes