@kokosro/ioredis-lock
v0.0.1
Published
A lightweight, distributed lock implementation for Node.js using Redis and ioredis. This package provides a simple way to implement distributed locks across multiple processes or servers.
Downloads
44
Readme
@kokosro/ioredis-lock
A lightweight, distributed lock implementation for Node.js using Redis and ioredis. This package provides a simple way to implement distributed locks across multiple processes or servers.
Features
- 🔒 Distributed locking with Redis
- 🔄 Automatic retry with configurable frequency
- ⏱️ Configurable lock TTL and max wait time
- 🎯 Type-safe with TypeScript
- 🔧 Functional programming approach
- 🪵 Optional verbose logging
- 🎨 Customizable key namespacing
Installation
npm install @kokosro/ioredis-lock ioredisQuick Start
import { createLockService } from "@kokosro/ioredis-lock";
import Redis from "ioredis";
// Create a lock service
const lockService = await createLockService({
connection: new Redis(), // optional, creates new connection if not provided
ttlSeconds: 10, // default: 10
maxWaitMilliseconds: 10000, // default: 10000
frequencyMilliseconds: 1000, // default: 1000
});
// Acquire and release a lock manually
const release = await lockService.acquireLock("my-resource");
try {
// Do some work with exclusive access
console.log("Lock acquired!");
} finally {
await release();
}API
createLockService(options)
Creates a new lock service instance.
Options
| Option | Type | Default | Description |
| ----------------------- | ----------------------- | ------------- | ------------------------------------------------------------------ |
| connection | Redis \| RedisOptions | new Redis() | ioredis connection instance or connection config object |
| frequencyMilliseconds | number | 1000 | How often to retry acquiring the lock (in milliseconds) |
| ttlSeconds | number | 10 | Time-to-live for the lock (in seconds) |
| maxWaitMilliseconds | number | 10000 | Maximum time to wait for lock acquisition before throwing an error |
| verbose | boolean | false | Enable verbose logging |
| namespacePrefix | string | 'lock' | Prefix for all lock keys in Redis |
| keySeparator | string | ':' | Separator between namespace and key |
RedisOptions (Connection Config)
When passing a connection config object, you can use any option from the ioredis RedisOptions. Here are the most commonly used options:
| Option | Type | Description |
| ---------------------- | ------------------------------------------- | ------------------------------------------- |
| host | string | Redis server host |
| port | number | Redis server port |
| username | string | Redis username (Redis 6+) |
| password | string | Redis password |
| db | number | Redis database number |
| tls | boolean \| { key, cert, ca } | Enable TLS/SSL connection |
| connectTimeout | number | Connection timeout in milliseconds |
| keepAlive | number | TCP keep-alive interval in milliseconds |
| family | 4 \| 6 | IP version (IPv4 or IPv6) |
| retryStrategy | (times: number) => number \| void \| null | Custom retry strategy function |
| maxRetriesPerRequest | number \| null | Max retries per request, null for unlimited |
| enableOfflineQueue | boolean | Queue commands when connection is down |
| lazyConnect | boolean | Don't auto-connect on instantiation |
| connectionName | string | Connection name for debugging |
| readOnly | boolean | Enable read-only mode |
Example with connection config:
const lockService = await createLockService({
connection: {
host: "redis.example.com",
port: 6380,
password: "your-password",
tls: true,
connectTimeout: 10000,
retryStrategy: (times) => Math.min(times * 50, 2000),
},
ttlSeconds: 30,
});Returns
A LockService object with the following methods:
lockService.acquireLock(key: string)
Acquires a lock for the given key. Waits until the lock is available or times out.
Parameters:
key(string): The key to lock
Returns: Promise<() => Promise<number>> - A release function that returns 1 if successfully released, 0 otherwise
Example:
const release = await lockService.acquireLock("user:123");
try {
// Critical section
} finally {
await release();
}lockService.exclusiveSet<T>(key: string, fn: (value: T) => Promise<any>)
Acquires a lock, gets the current value from Redis, runs a function with that value, and sets the result back to Redis.
Parameters:
key(string): The Redis key to read and writefn(function): A function that receives the current value and returns the new value
Returns: Promise<any> - The new value that was set
Example:
// Increment a counter atomically
await lockService.exclusiveSet("counter", async (currentValue) => {
const current = parseInt(currentValue || "0");
return (current + 1).toString();
});lockService.exclusiveRun<T>(key: string, fn: () => Promise<T>)
Acquires a lock and runs a function with exclusive access. Useful when you want to run arbitrary code while holding a lock.
Parameters:
key(string): The lock keyfn(function): The function to run while holding the lock
Returns: Promise<T | undefined> - The result of the function
Example:
const result = await lockService.exclusiveRun("process-orders", async () => {
// Process orders with exclusive access
const orders = await getOrders();
await processOrders(orders);
return orders.length;
});Use Cases
Preventing Concurrent Operations
// Ensure only one instance processes a user's data at a time
await lockService.exclusiveRun(`process-user:${userId}`, async () => {
const userData = await fetchUserData(userId);
await processUserData(userData);
await saveUserData(userId, userData);
});Atomic Counters
// Implement a distributed counter
const newCount = await lockService.exclusiveSet("page-views", async (value) => {
return (parseInt(value || "0") + 1).toString();
});Resource Pool Management
// Ensure only one process modifies a resource pool
const resource = await lockService.exclusiveRun("resource-pool", async () => {
const pool = await getAvailableResources();
const resource = pool.pop();
await updateResourcePool(pool);
return resource;
});How It Works
The package uses Lua scripts to ensure atomic operations in Redis:
- Acquire Lock: Uses
SET key value EX ttl NXpattern via Lua script to atomically set a lock with a TTL if it doesn't exist - Release Lock: Verifies the lock value matches before deleting to ensure only the lock holder can release it
- Retry Logic: Automatically retries lock acquisition at the configured frequency until successful or timeout
Each lock is identified by a unique UUID, ensuring that only the process that acquired the lock can release it.
Error Handling
The lock service throws an error if:
- Unable to acquire lock within
maxWaitMilliseconds - User-provided function throws an error (in
exclusiveSetandexclusiveRun)
try {
await lockService.acquireLock("my-key");
} catch (error) {
console.error("Failed to acquire lock:", error.message);
// Handle timeout or other errors
}Best Practices
- Always release locks: Use try-finally blocks to ensure locks are released
- Set appropriate TTL: Choose a TTL longer than your critical section execution time
- Handle timeouts: Be prepared for lock acquisition timeouts in high-contention scenarios
- Use descriptive keys: Name your locks clearly to avoid conflicts
- Monitor lock duration: Enable
verbosemode during development to monitor lock timing
License
AGPL-3.0
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
