UNPKG

@ndustrial/node-distributed-lock

Version:

Enables distributed locking for sequelize applications

129 lines (112 loc) 3.66 kB
const { v4: uuidv4 } = require('uuid'); const exitHook = require('async-exit-hook'); const { callbackify } = require('util'); const Mutex = require('./mutex'); const { AlreadyObtainedError, TableLockedError, LockTimeoutError } = require('./error'); const log = require('./utils/log'); const DEFAULT_LOCK_TABLE_NAME = 'distributed_lock'; const DEFAULT_LOCK_TIMEOUT_SEC = 300; const DEFAULT_SLEEP_MSEC = 1000; const defaultParams = { lockTableName: DEFAULT_LOCK_TABLE_NAME, lockTTLSeconds: 1200, skipIfObtained: false }; const currentLocks = {}; const onExit = callbackify(async () => { log('Cleaning up any held locks...'); // eslint-disable-next-line no-restricted-syntax for (const currentLock of Object.values(currentLocks)) { const { mutex, lockName, nodeId } = currentLock; log(`[${nodeId}]: Releasing lock ${lockName}`); try { await mutex.releaseLock(lockName, nodeId); delete currentLock[nodeId]; log( `[${nodeId}]: Successfully released lock ${this.lockName}` ); } catch (e) { console.error(`Unable to release lock. ${e.message}`); } } }); // Attempt to release any held locks if we are accidentally killed exitHook((done) => { if (!done) { log('Skipping lock cleanup due to synchronous exit.'); return; } return onExit(done); }); class DistributedLock { constructor(lockName, params) { this.nodeId = uuidv4(); this.lockName = lockName; const { queryInterface, lockTableName, lockTTLSeconds, skipIfObtained, queryInterfaceName } = { ...defaultParams, ...params }; this.skipIfObtained = skipIfObtained; if (!queryInterface) { throw new Error('Query interface required'); } this.mutex = new Mutex({ queryInterface, lockTableName, lockTTLSeconds, queryInterfaceName }); log(`[${this.nodeId}]: Initialized instance of distributed lock`); } async lock( execute = () => Promise.resolve(), params = {} ) { const { timeoutSeconds = DEFAULT_LOCK_TIMEOUT_SEC, sleepMilliseconds = DEFAULT_SLEEP_MSEC } = params; await this.mutex.initializeLockTable(); const until = Date.now() + timeoutSeconds * 1000; while (true) { try { log( `[${this.nodeId}]: Attempting to obtain lock ${this.lockName}` ); await this.mutex.obtainLock(this.lockName, this.nodeId); break; } catch (e) { if ( !(e instanceof AlreadyObtainedError || e instanceof TableLockedError) ) { throw e; } if (e instanceof AlreadyObtainedError && this.skipIfObtained) { log(`[${this.nodeId}]: Lock has been obtained. Exiting...`); return DistributedLock.EXECUTION_SKIPPED; } log(`[${this.nodeId}]: Unable to obtain lock: ${e.message}`); // eslint-disable-next-line no-promise-executor-return await (new Promise((resolve) => setTimeout(resolve, sleepMilliseconds))); if (Date.now() >= until) { throw new LockTimeoutError(this.lockName, timeoutSeconds); } } } log( `[${this.nodeId}]: Successfully obtained lock ${this.lockName}` ); currentLocks[this.nodeId] = this; try { return await execute(); } finally { log(`[${this.nodeId}]: Releasing lock ${this.lockName}`); await this.mutex.releaseLock(this.lockName, this.nodeId); delete currentLocks[this.nodeId]; log( `[${this.nodeId}]: Successfully released lock ${this.lockName}` ); } } } DistributedLock.EXECUTION_SKIPPED = Symbol('ExecutionSkipped'); module.exports = DistributedLock;