node-redlock
Version:
A distributed locking algorithm used to manage distributed resources in a system.
161 lines (141 loc) • 5.85 kB
text/typescript
// Redlock.ts
import { RedisClient } from './interfaces/RedisClient';
interface Lock {
resource: string;
value: string;
ttl: number;
}
export class Redlock {
private clients: RedisClient[];
private retryCount: number;
private retryDelay: number;
private driftFactor: number;
constructor(clients: RedisClient[], retryCount = 3, retryDelay = 200, driftFactor = 0.01) {
this.clients = clients;
this.retryCount = retryCount;
this.retryDelay = retryDelay;
this.driftFactor = driftFactor;
}
/**
* Attempts to acquire a lock on a single Redis client.
* @param client - The Redis client.
* @param resource - The resource to lock.
* @param value - The unique lock value.
* @param ttl - The time-to-live for the lock in milliseconds.
* @returns A promise that resolves to true if the lock was acquired, false otherwise.
*/
private async acquireLockInstance(client: RedisClient, resource: string, value: string, ttl: number): Promise<boolean> {
const result = await client.set(resource, value, { NX: true, PX: ttl });
return result === 'OK';
}
/**
* Releases a lock on a single Redis client.
* @param client - The Redis client.
* @param lock - The lock to release.
* @returns A promise that resolves to the number of keys that were removed.
*/
private releaseLockInstance(client: RedisClient, lock: Lock): Promise<number> {
const script = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`;
return client.eval(script, [lock.resource], [lock.value]);
}
/**
* Attempts to acquire a lock on the specified resource.
* @param resource - The resource to lock.
* @param ttl - The time-to-live for the lock in milliseconds.
* @returns A promise that resolves to the acquired lock or null if the lock could not be acquired.
*/
public async acquireLock(resource: string, ttl: number): Promise<Lock | null> {
const value = Math.random().toString(36).substring(2, 15); // Unique lock value
const end = Date.now() + ttl;
for (let i = 0; i < this.retryCount; i++) {
const startTime = Date.now();
const successCount = await this.tryAcquire(resource, value, ttl);
const elapsedTime = Date.now() - startTime;
const valid = elapsedTime < ttl * (1 - this.driftFactor);
if (successCount >= Math.floor(this.clients.length / 2) + 1 && valid) {
return { resource, value, ttl: end };
}
await this.releaseLock(resource, value);
await this.sleep(this.retryDelay);
}
return null;
}
/**
* Attempts to acquire a lock on the specified resource using a custom retry strategy.
* @param resource - The resource to lock.
* @param ttl - The time-to-live for the lock in milliseconds.
* @param retryStrategy - A function that determines the delay before the next retry attempt.
* @returns A promise that resolves to the acquired lock or null if the lock could not be acquired.
*/
public async acquireLockWithCustomRetry(resource: string, ttl: number, retryStrategy: (attempt: number) => number): Promise<Lock | null> {
const value = Math.random().toString(36).substring(2, 15);
const end = Date.now() + ttl;
for (let i = 0; i < this.retryCount; i++) {
const successCount = await this.tryAcquire(resource, value, ttl);
if (successCount >= Math.floor(this.clients.length / 2) + 1) {
return { resource, value, ttl: end };
}
await this.releaseLock(resource, value);
await this.sleep(retryStrategy(i));
}
return null;
}
/**
* Attempts to acquire a lock on the specified resource across all clients.
* @param resource - The resource to lock.
* @param value - The unique lock value.
* @param ttl - The time-to-live for the lock in milliseconds.
* @returns A promise that resolves to the number of successful lock acquisitions.
*/
private async tryAcquire(resource: string, value: string, ttl: number): Promise<number> {
const promises = this.clients.map(client => this.acquireLockInstance(client, resource, value, ttl));
const results = await Promise.all(promises);
return results.filter(result => result).length;
}
/**
* Releases a lock on the specified resource.
* @param resource - The resource to unlock.
* @param value - The unique lock value.
* @returns A promise that resolves when the lock has been released.
*/
public async releaseLock(resource: string, value: string): Promise<void> {
const lock: Lock = { resource, value, ttl: 0 };
const promises = this.clients.map(client => this.releaseLockInstance(client, lock));
await Promise.all(promises);
}
/**
* Renews a lock on the specified resource.
* @param resource - The resource to renew the lock on.
* @param value - The unique lock value.
* @param ttl - The new time-to-live for the lock in milliseconds.
* @returns A promise that resolves to true if the lock was renewed, false otherwise.
*/
public async renewLock(resource: string, value: string, ttl: number): Promise<boolean> {
const script = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("pexpire", KEYS[1], ARGV[2])
else
return 0
end
`;
const promises = this.clients.map(client =>
client.eval(script, [resource], [value, ttl.toString()])
);
const results = await Promise.all(promises);
return results.every(result => result === 1);
}
/**
* Sleeps for the specified number of milliseconds.
* @param ms - The number of milliseconds to sleep.
* @returns A promise that resolves after the specified delay.
*/
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}