@shagital/atomic-lock
Version:
Universal atomic locking with pluggable drivers (Redis, SQLite, File, Memory)
148 lines (147 loc) • 5.17 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AtomicLock = void 0;
exports.createLock = createLock;
const redis_driver_1 = require("../drivers/redis-driver");
const file_driver_1 = require("../drivers/file-driver");
const sqlite_driver_1 = require("../drivers/sqlite-driver");
const memory_driver_1 = require("../drivers/memory-driver");
const uuid_1 = require("uuid");
/**
* Universal atomic lock with memory-safe circuit breaker
*/
class AtomicLock {
/**
* Callback-style lock usage. Acquires the lock, runs the callback, always releases the lock.
* Throws if lock cannot be acquired. Returns the callback's result.
*/
async withLock(key, callback, options = {}) {
const lockValue = await this.acquire(key, options);
try {
return await callback(lockValue);
}
finally {
await this.release(key, lockValue);
}
}
constructor(config, options = {}) {
this.lockFailures = new Map();
this.lastCleanup = Date.now();
this.driver = this.createDriver(config);
this.circuitBreakerThreshold = options.circuitBreakerThreshold ?? 5;
this.circuitBreakerResetTime = options.circuitBreakerResetTime ?? 30000;
this.maxFailureEntries = options.maxFailureEntries ?? 1000;
}
createDriver(config) {
switch (config.driver) {
case 'redis':
return new redis_driver_1.RedisLockDriver(config.redis);
case 'file':
return new file_driver_1.FileLockDriver(config.file);
case 'sqlite':
return new sqlite_driver_1.SQLiteLockDriver(config.sqlite);
case 'memory':
return new memory_driver_1.MemoryLockDriver(config.memory);
default:
throw new Error(`Unsupported driver: ${config.driver}`);
}
}
async tryAcquire(key, options = {}) {
const expiryInSeconds = options.expiryInSeconds ?? 10;
if (this.isCircuitOpen(key)) {
return null;
}
const lockValue = this.generateLockValue();
try {
const acquired = await this.driver.tryAcquire(key, lockValue, expiryInSeconds);
if (acquired) {
this.lockFailures.delete(key);
return lockValue;
}
return null;
}
catch (error) {
this.recordLockFailure(key);
return null;
}
}
async acquire(key, options = {}) {
const lockValue = await this.tryAcquire(key, options);
if (!lockValue) {
throw new Error(`Failed to acquire lock for key: ${key}`);
}
return lockValue;
}
async release(key, lockValue) {
try {
return await this.driver.release(key, lockValue);
}
catch (error) {
console.error(`Error releasing lock ${key}:`, error);
return false;
}
}
getCircuitBreakerStatus(key) {
const failure = this.lockFailures.get(key);
const isOpen = this.isCircuitOpen(key);
return {
isOpen,
failureCount: failure?.count ?? 0,
lastFailure: failure?.lastFailure,
nextAttemptAt: failure ? failure.lastFailure + this.circuitBreakerResetTime : undefined
};
}
isCircuitOpen(key) {
const failure = this.lockFailures.get(key);
if (!failure)
return false;
const isOverThreshold = failure.count >= this.circuitBreakerThreshold;
const isWithinResetTime = (Date.now() - failure.lastFailure) < this.circuitBreakerResetTime;
return isOverThreshold && isWithinResetTime;
}
recordLockFailure(key) {
const now = Date.now();
if (now - this.lastCleanup > 60000) {
this.cleanupFailureRecords();
this.lastCleanup = now;
}
if (this.lockFailures.size >= this.maxFailureEntries) {
const oldestKeys = Array.from(this.lockFailures.keys()).slice(0, 100);
oldestKeys.forEach(key => this.lockFailures.delete(key));
}
const existing = this.lockFailures.get(key);
if (existing) {
if (now - existing.lastFailure > this.circuitBreakerResetTime) {
existing.count = 1;
}
else {
existing.count++;
}
existing.lastFailure = now;
}
else {
this.lockFailures.set(key, {
count: 1,
lastFailure: now
});
}
}
cleanupFailureRecords() {
const now = Date.now();
for (const [key, failure] of this.lockFailures.entries()) {
if (now - failure.lastFailure > this.circuitBreakerResetTime) {
this.lockFailures.delete(key);
}
}
}
generateLockValue() {
return (0, uuid_1.v4)();
}
async close() {
this.lockFailures.clear();
}
}
exports.AtomicLock = AtomicLock;
function createLock(config, options) {
return new AtomicLock(config, options);
}