A comprehensive TypeScript utility library featuring Redis distributed locking and more. Designed for scalable applications requiring reliable concurrency control and data management utilities.
- 🔒 Distributed Locking: Redis-based distributed locks with RedLock algorithm
- 🔄 Auto-Retry & Extension: Smart retry mechanism and lock extension for long operations
- ⚡ Timeout Management: Sophisticated timeout handling with automatic error recovery
- 🛡️ Error Resilience: Comprehensive error handling with customizable callbacks
- 🚀 High Performance: Optimized for high-concurrency environments
- 📦 TypeScript First: Full type safety with zero external dependencies
npm install utilary redis
import { createClient } from 'redis'
import { RedLock } from 'utilary'
// Initialize Redis client
const client = createClient({
url: 'redis://localhost:6379',
})
await client.connect()
// Create RedLock instance with advanced configuration
const redlock = new RedLock(client, {
retryCount: 5, // Maximum retry attempts
retryDelay: 100, // Base delay between retries (ms)
retryJitter: 50, // Random delay variation
driftFactor: 0.01, // Clock drift compensation
automaticExtensionThreshold: 500, // Auto-extension threshold
onError: error => {
console.error('🚨 RedLock operation failed:', error.message)
},
})
// Simple operations with automatic lock handling
await redlock.lock('payment-processing', 3000, async () => {
await processPayment()
// Lock is automatically released after completion
})
// Long-running operations with controlled extensions
await redlock.autoExtendLock('report-generation', 2000, async () => {
await generateLargeReport()
// Lock automatically extends up to 5 times
}, { maxExtensions: 5 })
// Payment processing with strict limit
await redlock.autoExtendLock('payment-critical', 1500, async () => {
await processPayment()
// Only 1 extension allowed for predictable timing
}, { maxExtensions: 1 })
// Background sync with unlimited extensions
await redlock.autoExtendLock('data-sync', 3000, async () => {
await syncLargeDataset()
// Unlimited extensions for unpredictable operations
}, { maxExtensions: -1 })
// Custom extension threshold for early triggers
await redlock.autoExtendLock('custom-operation', 2000, async () => {
await performOperation()
// Extend when 1000ms left instead of default 500ms
}, {
maxExtensions: 3,
extensionThreshold: 1000
})
// Fine-grained control over lock lifecycle
const lock = await redlock.acquire('user-data', 5000)
if (lock) {
try {
await performOperation()
await redlock.extend(lock, 3000) // Extend if needed
await continueOperation()
} finally {
await redlock.release(lock)
}
}
RedLock provides intelligent extension control with flexible configuration:
-
Unlimited Extensions (default behavior)
await redlock.autoExtendLock('operation', 2000, fn) // No options = unlimited await redlock.autoExtendLock('operation', 2000, fn, { maxExtensions: -1 })
-
Limited Extensions
await redlock.autoExtendLock('payment', 3000, fn, { maxExtensions: 2 }) // Will extend maximum 2 times, then let TTL expire naturally
-
No Extensions
await redlock.autoExtendLock('quick-task', 1000, fn, { maxExtensions: 0 }) // Behaves like regular lock() with fixed TTL
Control when extensions are triggered:
await redlock.autoExtendLock('operation', 5000, fn, {
maxExtensions: 3,
extensionThreshold: 1500 // Extend when 1500ms left (vs default 500ms)
})
-
Payment Processing:
maxExtensions: 1-2
(predictable timing) -
Data Migration:
maxExtensions: 5-10
(reasonable safety margin) -
Background Jobs:
maxExtensions: -1
(unlimited for reliability) -
Critical Operations:
maxExtensions: 0
(strict time bounds)
RedLock implements an intelligent retry mechanism with:
-
Exponential Backoff
- Increasing delays between retries
- Prevents system overload
- Example: 100ms → 200ms → 400ms → 800ms
-
Random Jitter
- Prevents thundering herd problem
- Improves concurrent operation handling
- Reduces network congestion
try {
await redlock.lock('critical-operation', 2000, async () => {
await criticalTask()
})
} catch (error) {
if (error instanceof LockTimeoutError) {
// Handle timeout - operation exceeded duration
await handleTimeout()
} else if (error instanceof LockAcquisitionError) {
// Handle acquisition failure - retries exhausted
await handleAcquisitionFailure()
}
}
const redlock = new RedLock(client, {
onError: (error) => {
if (error instanceof LockTimeoutError) {
metrics.increment('lock.timeouts')
logger.warn('Lock timeout', {
resource: error.resource,
duration: error.duration
})
}
}
})
💡 Want more examples? Check out our complete example collection with detailed use cases and advanced patterns.
async function processPaymentWithRetry(userId: string, amount: number) {
const LOCK_DURATION = 5000
const MAX_RETRIES = 3
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
await redlock.lock(`payment-${userId}`, LOCK_DURATION, async () => {
await validateBalance(userId)
await deductAmount(userId, amount)
await createTransaction(userId, amount)
})
return true
} catch (error) {
if (error instanceof LockTimeoutError) {
if (attempt === MAX_RETRIES) throw error
await delay(1000 * attempt) // Exponential backoff
} else {
throw error
}
}
}
}
// Data migration with reasonable extension limits
await redlock.autoExtendLock('user-migration', 2000, async () => {
const userData = await fetchUserData()
await processLargeDataset(userData)
await saveUpdatedData(userData)
}, { maxExtensions: 10 }) // Allow up to 10 extensions for large operations
// Critical financial operations with strict control
await redlock.autoExtendLock('financial-calc', 3000, async () => {
await performComplexCalculation()
await updateAccountBalances()
}, {
maxExtensions: 2, // Maximum 2 extensions
extensionThreshold: 800 // Extend early for safety
})
Parameter | Type | Default | Description |
---|---|---|---|
maxExtensions |
number |
undefined |
Maximum extensions allowed (-1 = unlimited, 0 = none, >0 = limit) |
extensionThreshold |
number |
100 |
Time remaining (ms) when extension is triggered |
// 💳 Payment processing (predictable timing)
await redlock.autoExtendLock('payment', 3000, paymentFn, {
maxExtensions: 1
})
// 📊 Data processing (reasonable safety)
await redlock.autoExtendLock('analytics', 5000, analyticsFn, {
maxExtensions: 5
})
// 🔄 Background sync (maximum reliability)
await redlock.autoExtendLock('sync', 2000, syncFn, {
maxExtensions: -1
})
// ⚡ Quick operations (no extensions)
await redlock.autoExtendLock('cache-refresh', 1000, cacheFn, {
maxExtensions: 0
})
// 🎯 Custom threshold (extend early)
await redlock.autoExtendLock('critical', 4000, criticalFn, {
maxExtensions: 2,
extensionThreshold: 1500 // Extend when 1.5s left
})
// ✅ CORRECT: Proper error handling with cleanup
async function processWithLock(userId: string) {
let transaction = null
let timer = null
try {
// Start transaction
transaction = await db.beginTransaction()
// Set operation timeout
timer = setTimeout(() => {
throw new Error('Operation timeout')
}, 30000)
await redlock.lock(`user-${userId}`, 5000, async () => {
await updateUserData(userId, transaction)
await transaction.commit()
})
} catch (error) {
// Handle lock failures, timeouts, or operation errors
if (transaction) {
await transaction.rollback()
console.log('Transaction rolled back due to error')
}
if (error instanceof LockTimeoutError) {
console.error('Lock operation timed out:', error.message)
// Handle timeout-specific cleanup
} else if (error instanceof LockAcquisitionError) {
console.error('Failed to acquire lock:', error.message)
// Handle acquisition failure
} else {
console.error('Unexpected error:', error.message)
}
throw error // Re-throw if needed
} finally {
// Always cleanup resources
if (timer) {
clearTimeout(timer)
}
// Close connections, cleanup resources, etc.
await cleanupResources()
}
}
// ❌ INCORRECT: No error handling - can cause resource leaks
async function unsafeProcess(userId: string) {
const transaction = await db.beginTransaction()
await redlock.lock(`user-${userId}`, 5000, async () => {
await updateUserData(userId, transaction)
await transaction.commit()
})
// If lock fails, transaction is never rolled back!
}
-
Database Transactions
const transaction = await db.beginTransaction() try { await redlock.lock('resource', 3000, async () => { await performDatabaseOperations(transaction) await transaction.commit() }) } catch (error) { await transaction.rollback() throw error }
-
Timeout Management
const timer = setTimeout(() => controller.abort(), 30000) try { await redlock.lock('resource', 5000, async () => { await operationWithAbortSignal(controller.signal) }) } finally { clearTimeout(timer) }
-
Resource Cleanup
let fileHandle = null try { fileHandle = await openFile('data.txt') await redlock.lock('file-processing', 3000, async () => { await processFile(fileHandle) }) } catch (error) { // Handle errors appropriately await handleProcessingError(error) } finally { if (fileHandle) { await fileHandle.close() } }
try {
await redlock.autoExtendLock('critical-operation', 2000, async () => {
await performCriticalTask()
}, { maxExtensions: 3 })
} catch (error) {
if (error instanceof LockTimeoutError) {
// Operation exceeded maximum time (initial TTL + extensions)
await rollbackOperations()
await notifyTimeout()
} else if (error instanceof LockAcquisitionError) {
// Could not acquire lock after retries
await handleAcquisitionFailure()
await scheduleRetry()
} else if (error instanceof LockExtensionError) {
// Lock extension failed (Redis connection issues, etc.)
await handleExtensionFailure()
await emergencyCleanup()
}
}
-
Lock Duration & Extension Strategy
- Set appropriate initial TTL based on expected operation time
- Use
maxExtensions
to control resource usage:-
Critical/Financial:
maxExtensions: 1-2
for predictable timing -
Data Processing:
maxExtensions: 5-10
for reasonable safety -
Background Tasks:
maxExtensions: -1
for maximum reliability
-
Critical/Financial:
- Prefer shorter TTL with extensions over very long initial TTL
- Include buffer for network latency
-
Resource Keys
- Use descriptive, hierarchical keys
- Include relevant identifiers (user ID, transaction ID, etc.)
- Consider environment prefixes (
prod:payment:
,dev:sync:
)
-
Extension Configuration
- Set
extensionThreshold
based on operation criticality:- High-priority: 1000ms+ (extend early)
- Normal operations: 500ms (default)
- Quick tasks: 100-200ms (minimal extension window)
- Monitor extension patterns to optimize thresholds
- Set
-
Error Handling & Thread Safety
- ALWAYS wrap RedLock operations in try-catch blocks
- Implement comprehensive error catching for all lock operations
- Use proper cleanup in finally blocks for transactions, timers, and resources
- Handle different error types appropriately (timeout, acquisition, extension failures)
- Ensure database transactions are rolled back on lock failures
- Clear timeouts and cleanup resources in finally blocks
- Monitor and log lock failures, especially extension failures
- Handle max extension limits gracefully
-
Performance
- Keep lock durations minimal while allowing for extensions
- Release locks as soon as possible
- Use appropriate retry configurations
- Monitor extension frequency to detect performance issues
We welcome contributions! Please read our Contributing Guide for details.
- Request Features: Open an issue on GitHub
- Report Bugs: Submit a bug report
Licensed under the Apache License, Version 2.0. See LICENSE for details.
Developed by Hunandika - Building reliable utilities for modern applications.
Utilary: Where utility meets reliability