import { createLoggerBundle } from '@gongt/ts-stl-library/debug/create-logger'; import DI, { DDNames } from '@gongt/ts-stl-library/DI'; import * as Redlock from 'redlock'; import { connectToRedis, RedisConfig } from '../redis/connect'; const logger = createLoggerBundle('redlock'); export type UnlockFunction = () => Promise export function connectToRedisLocker(cfg: RedisConfig) { DI.set(DDNames.redisLockServer, connectToRedis(cfg)); } export class AsyncLocker { protected static server: Redlock; MAX_HOLD = 30000; TIMEOUT = 5000; protected holdList: {[id: string]: NodeJS.Timer}; private lockCount: number; protected lockTimer: NodeJS.Timer; protected lockerList: {[id: string]: Redlock.Lock}; constructor(private resource: string) { if (!AsyncLocker.server) { const redis = DI.get(DDNames.redisLockServer); AsyncLocker.server = new Redlock([redis], { retryCount: 3, retryDelay: 1000, }); AsyncLocker.server.on('clientError', function (err) { logger.error('A redis error has occurred:', err); }); } this.lockCount = 0; this.lockerList = {}; this.holdList = {}; } async lock(name: string = 'root') { logger.debug('get lock for %s:%s', this.resource, name); const lock = await AsyncLocker.server.lock(this.resource + ':' + name, this.TIMEOUT); this.lockerList[name] = lock; this.preventTooLong(lock); this.increaseRenew(); return this.unlock.bind(this, name); } async unlock(name: string = 'root') { const lock = this.lockerList[name]; delete this.lockerList[name]; if (!lock) { logger.warn('duplicate release lock for %s:%s', this.resource, name); return; } await lock.unlock(); logger.debug('release lock for %s:%s', this.resource, name); this.decreaseRenew(); this.notTooLong(lock); } protected decreaseRenew() { this.lockCount--; if (this.lockCount === 0) { clearInterval(this.lockTimer); this.lockTimer = null; } } protected increaseRenew() { if (this.lockCount === 0) { this.lockTimer = setInterval(() => { for (const lock of Object.values(this.lockerList)) { logger.debug('extend lock for %s', lock.resource); lock.extend(this.TIMEOUT).catch((e) => { logger.error('extend lock failed: ' + lock.resource); }); } }, Math.round(this.TIMEOUT / 2)); } this.lockCount++; } private notTooLong(lock: Redlock.Lock) { if (this.holdList.hasOwnProperty(lock.resource)) { clearTimeout(this.holdList[lock.resource]); delete this.holdList[lock.resource]; } } private preventTooLong(lock: Redlock.Lock) { this.holdList[lock.resource] = setTimeout(() => { delete this.holdList[lock.resource]; logger.error('lock hold too long, force release: %s', lock.resource); lock.unlock().then(() => { logger.error('force released lock: %s', lock.resource); }, (e) => { logger.error('force release lock %s failed: %s', lock.resource, e.message); }); }, this.MAX_HOLD); } }