rwlockfile
Version:
lockfile utility with reader/writers
403 lines (402 loc) • 13.1 kB
JavaScript
"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;