Initial Commit
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
DEFAULT_LOCK_KEY,
|
||||
DEFAULT_LOCK_TIMEOUT_MS,
|
||||
DEFAULT_LOCK_TTL_MS,
|
||||
} from "../../constants/index.js";
|
||||
import type {
|
||||
DistributedLockAdapter,
|
||||
DistributedLockConfig,
|
||||
MigrationLockAdapter,
|
||||
} from "../../types/migration.types.js";
|
||||
|
||||
/**
|
||||
* In-memory implementation of the distributed lock adapter interface.
|
||||
* Suitable for testing without a real Redis or database backend.
|
||||
*/
|
||||
export class InMemoryDistributedLockAdapter implements DistributedLockAdapter {
|
||||
protected readonly locks: Map<string, number> = new Map();
|
||||
|
||||
async acquire(key: string, ttl: number): Promise<boolean> {
|
||||
const now = Date.now();
|
||||
const existing = this.locks.get(key);
|
||||
if (existing !== undefined && existing > now) {
|
||||
return false;
|
||||
}
|
||||
this.locks.set(key, now + ttl);
|
||||
return true;
|
||||
}
|
||||
|
||||
async release(key: string): Promise<void> {
|
||||
this.locks.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Distributed advisory lock that delegates acquisition and release to a
|
||||
* `DistributedLockAdapter`. Supports timeout-based retry.
|
||||
*/
|
||||
export class DistributedMigrationLock implements MigrationLockAdapter {
|
||||
protected readonly adapter: DistributedLockAdapter;
|
||||
protected readonly key: string;
|
||||
protected readonly ttl: number;
|
||||
|
||||
constructor(config: DistributedLockConfig) {
|
||||
this.adapter = config.adapter;
|
||||
this.key = config.key ?? DEFAULT_LOCK_KEY;
|
||||
this.ttl = config.ttl ?? DEFAULT_LOCK_TTL_MS;
|
||||
}
|
||||
|
||||
async acquire(timeoutMs = DEFAULT_LOCK_TIMEOUT_MS): Promise<boolean> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
let delay = 50;
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
const acquired = await this.adapter.acquire(this.key, this.ttl);
|
||||
if (acquired) return true;
|
||||
await sleep(Math.min(delay, deadline - Date.now()));
|
||||
delay = Math.min(delay * 2, 1000);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async release(): Promise<void> {
|
||||
await this.adapter.release(this.key);
|
||||
}
|
||||
|
||||
async isLocked(): Promise<boolean> {
|
||||
// Try to acquire with zero ttl — if it fails, something holds the lock
|
||||
const canAcquire = await this.adapter.acquire(`${this.key}__probe`, 0);
|
||||
if (canAcquire) {
|
||||
await this.adapter.release(`${this.key}__probe`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
||||
}
|
||||
Reference in New Issue
Block a user