UNPKG

rwlockfile

Version:

lockfile utility with reader/writers

403 lines (402 loc) 13.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const decorators_1 = require("./decorators"); const path = require("path"); const lockfile_1 = require("./lockfile"); const errors_1 = require("./errors"); const isProcessActive = require("is-process-active"); const version = require('../package.json').version; class RWLockfile { /** * creates a new read/write lockfile * @param base {string} - base filepath to create lock from */ constructor(base, options = {}) { this._count = { read: 0, write: 0 }; this.base = base; this._debug = options.debug || (debugEnvVar() && require('debug')('rwlockfile')); this.uuid = require('uuid/v4')(); this.fs = require('fs-extra'); this.timeout = options.timeout || 30000; this.retryInterval = options.retryInterval || 10; this.ifLocked = options.ifLocked || (() => { }); instances.push(this); this.internal = new lockfile_1.default(this.file, { debug: debugEnvVar() === 2 && this._debug, }); } get count() { return { read: this._count.read, write: this._count.write }; } get file() { return path.resolve(this.base + '.lock'); } async add(type, opts = {}) { this._debugReport('add', type, opts); if (!this._count[type]) await this._lock(type, opts); this._count[type]++; } addSync(type, opts = {}) { this._debugReport('addSync', type, opts); this._lockSync(type, opts.reason); } async remove(type) { this._debugReport('remove', type); switch (this.count[type]) { case 0: break; case 1: await this.unlock(type); break; default: this._count[type]--; break; } } removeSync(type) { this._debugReport('removeSync', type); switch (this.count[type]) { case 0: break; case 1: this.unlockSync(type); break; default: this._count[type]--; break; } } async unlock(type) { if (!type) { await this.unlock('read'); await this.unlock('write'); return; } if (!this.count[type]) return; await this._removeJob(type); this._count[type] = 0; } unlockSync(type) { if (!type) { this.unlockSync('write'); this.unlockSync('read'); return; } if (!this.count[type]) return; this._debugReport('unlockSync', type); this._removeJobSync(type); this._count[type] = 0; } async check(type) { const f = await this._fetchFile(); const status = this._statusFromFile(type, f); if (status.status === 'open') return status; else if (status.status === 'write_lock') { if (!await isActive(status.job.pid)) { this.debug(`removing inactive write pid: ${status.job.pid}`); delete f.writer; await this.writeFile(f); return this.check(type); } return status; } else if (status.status === 'read_lock') { const pids = await Promise.all(status.jobs.map(async (j) => { if (!await isActive(j.pid)) return j.pid; })); const inactive = pids.filter(p => !!p); if (inactive.length) { this.debug(`removing inactive read pids: ${inactive}`); f.readers = f.readers.filter(j => !inactive.includes(j.pid)); await this.writeFile(f); return this.check(type); } if (!status.jobs.find(j => j.uuid !== this.uuid)) return { status: 'open', file: this.file }; return status; } else throw new Error(`Unexpected status: ${status.status}`); } checkSync(type) { const f = this._fetchFileSync(); const status = this._statusFromFile(type, f); if (status.status === 'open') return status; else if (status.status === 'write_lock') { if (!isActiveSync(status.job.pid)) { this.debug(`removing inactive writer pid: ${status.job.pid}`); delete f.writer; this.writeFileSync(f); return this.checkSync(type); } return status; } else if (status.status === 'read_lock') { const inactive = status.jobs.map(j => j.pid).filter(pid => !isActiveSync(pid)); if (inactive.length) { this.debug(`removing inactive reader pids: ${inactive}`); f.readers = f.readers.filter(j => !inactive.includes(j.pid)); this.writeFileSync(f); return this.checkSync(type); } if (!status.jobs.find(j => j.uuid !== this.uuid)) return { status: 'open', file: this.file }; return status; } else throw new Error(`Unexpected status: ${status.status}`); } _statusFromFile(type, f) { if (type === 'write' && this.count.write) return { status: 'open', file: this.file }; if (type === 'read' && this.count.write) return { status: 'open', file: this.file }; if (f.writer) return { status: 'write_lock', job: f.writer, file: this.file }; if (type === 'write') { if (f.readers.length) return { status: 'read_lock', jobs: f.readers, file: this.file }; } return { status: 'open', file: this.file }; } _parseFile(input) { function addDate(job) { if (!job) return; return Object.assign({}, job, { created: new Date(job.created || 0) }); } return Object.assign({}, input, { writer: addDate(input.writer), readers: input.readers.map(addDate) }); } _stringifyFile(input) { function addDate(job) { if (!job) return; return Object.assign({}, job, { created: (job.created || new Date(0)).toISOString() }); } return Object.assign({}, input, { writer: addDate(input.writer), readers: (input.readers || []).map(addDate) }); } async _fetchFile() { try { let f = await this.fs.readJSON(this.file); return this._parseFile(f); } catch (err) { if (err.code !== 'ENOENT') this.debug(err); return { version, readers: [], }; } } _fetchFileSync() { try { let f = this.fs.readJSONSync(this.file); return this._parseFile(f); } catch (err) { if (err.code !== 'ENOENT') this.debug(err); return { version, readers: [], }; } } addJob(type, reason, f) { let job = { reason, pid: process.pid, created: new Date(), uuid: this.uuid, }; if (type === 'read') f.readers.push(job); else f.writer = job; } async _removeJob(type) { let f = await this._fetchFile(); this._removeJobFromFile(type, f); await this.writeFile(f); } _removeJobSync(type) { let f = this._fetchFileSync(); this._removeJobFromFile(type, f); this.writeFileSync(f); } _removeJobFromFile(type, f) { if (type === 'read') f.readers = f.readers.filter(r => r.uuid !== this.uuid); else if (f.writer && f.writer.uuid === this.uuid) delete f.writer; } async _lock(type, opts) { opts.timeout = opts.timeout || this.timeout; opts.retryInterval = opts.retryInterval || this.retryInterval; let ifLockedCb = once(opts.ifLocked || this.ifLocked); while (true) { try { await this.tryLock(type, opts.reason, false); return; } catch (err) { if (err.code !== 'ELOCK') throw err; await ifLockedCb(err.status); if (opts.timeout < 0) throw err; // try again const interval = random(opts.retryInterval / 2, opts.retryInterval * 2); await wait(interval); opts.timeout -= interval; opts.retryInterval *= 2; } } } async tryLock(type, reason, inc = true) { if (this.count[type]) { if (inc) this._count[type]++; return; } this.debug('tryLock', type, reason); const status = await this.check(type); if (status.status !== 'open') { this.debug('status: %o', status); throw new errors_1.RWLockfileError(status); } let f = await this._fetchFile(); this.addJob(type, reason, f); await this.writeFile(f); if (inc) this._count[type]++; this.debug('got %s lock for %s', type, reason); } _lockSync(type, reason) { if (this._count[type]) { this._count[type]++; return; } const status = this.checkSync(type); if (status.status !== 'open') { this.debug('status: %o', status); throw new errors_1.RWLockfileError(status); } let f = this._fetchFileSync(); this.addJob(type, reason, f); this.writeFileSync(f); this._count[type]++; this.debug('got %s lock for %s', type, reason); } async writeFile(f) { if (!f.writer && !f.readers.length) { await this.fs.remove(this.file); } else { await this.fs.outputJSON(this.file, this._stringifyFile(f)); } } writeFileSync(f) { if (!f.writer && !f.readers.length) { try { this.fs.unlinkSync(this.file); } catch (err) { if (err.code !== 'ENOENT') throw err; } } else { this.fs.outputJSONSync(this.file, this._stringifyFile(f)); } } get debug() { return this._debug || ((..._) => { }); } _debugReport(action, type, { reason } = {}) { const operator = (action.startsWith('unlock') && `-${this.count[type]}`) || (action.startsWith('remove') && '-1') || '+1'; const read = this.count['read'] + (type === 'read' ? operator : ''); const write = this.count['write'] + (type === 'write' ? operator : ''); reason = reason ? ` reason:${reason}` : ''; this.debug(`read:${read} write:${write}${reason} ${this.file}`); } } tslib_1.__decorate([ decorators_1.lockfile('internal') ], RWLockfile.prototype, "check", null); tslib_1.__decorate([ decorators_1.lockfile('internal', { sync: true }) ], RWLockfile.prototype, "checkSync", null); tslib_1.__decorate([ decorators_1.onceAtATime(0), decorators_1.lockfile('internal') ], RWLockfile.prototype, "_removeJob", null); tslib_1.__decorate([ decorators_1.lockfile('internal', { sync: true }) ], RWLockfile.prototype, "_removeJobSync", null); tslib_1.__decorate([ decorators_1.onceAtATime(0) ], RWLockfile.prototype, "_lock", null); tslib_1.__decorate([ decorators_1.lockfile('internal') ], RWLockfile.prototype, "tryLock", null); tslib_1.__decorate([ decorators_1.lockfile('internal', { sync: true }) ], RWLockfile.prototype, "_lockSync", null); exports.RWLockfile = RWLockfile; const instances = []; process.once('exit', () => { for (let i of instances) { try { i.unlockSync(); } catch (err) { } } }); function debugEnvVar() { return (((process.env.RWLOCKFILE_DEBUG === '1' || process.env.HEROKU_DEBUG_ALL) && 1) || (process.env.RWLOCKFILE_DEBUG === '2' && 2) || 0); } function wait(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function random(min, max) { return Math.floor(Math.random() * (max - min) + min); } function once(fn) { let ran = false; return ((...args) => { if (ran) return; ran = true; return fn(...args); }); } async function isActive(pid) { try { return await isProcessActive.isActive(pid); } catch (err) { console.error(err); return false; } } function isActiveSync(pid) { try { return isProcessActive.isActiveSync(pid); } catch (err) { console.error(err); return false; } } exports.default = RWLockfile;