UNPKG

@shagital/atomic-lock

Version:

Universal atomic locking with pluggable drivers (Redis, SQLite, File, Memory)

148 lines (147 loc) 5.17 kB
"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); }