Skip to content

Commit

Permalink
fix: fix mtime precision on some filesystems
Browse files Browse the repository at this point in the history
Closes #82, #87
  • Loading branch information
satazor committed Apr 2, 2019
1 parent c0cdea2 commit 20f5a8c
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 31 deletions.
51 changes: 20 additions & 31 deletions lib/lockfile.js
Expand Up @@ -4,6 +4,7 @@ const path = require('path');
const fs = require('graceful-fs');
const retry = require('retry');
const onExit = require('signal-exit');
const createMtimeChecker = require('./mtime-checker');

const locks = {};

Expand All @@ -24,9 +25,22 @@ function resolveCanonicalPath(file, options, callback) {
function acquireLock(file, options, callback) {
// Use mkdir to create the lockfile (atomic operation)
options.fs.mkdir(getLockFile(file, options), (err) => {
// If successful, we are done
if (!err) {
return options.fs.stat(getLockFile(file, options), callback);
// At this point, we acquired the lock!
// Initialize the mtime checker and set the mtime of the lockfile
const mtimeChecker = createMtimeChecker();
const mtime = mtimeChecker.getDate();

return options.fs.utimes(getLockFile(file, options), mtime, mtime, (err) => {
// If it failed, try to remove the lock..
if (err) {
options.fs.rmdir(getLockFile(file, options), () => {});

return callback(err);
}

callback(null, mtimeChecker);
});
}

// If error is not EEXIST then some other error occurred while locking
Expand Down Expand Up @@ -111,7 +125,7 @@ function updateLock(file, options) {
return updateLock(file, options);
}

const isMtimeOurs = lock.mtimeChecker(lock.mtime, stat.mtime);
const isMtimeOurs = lock.mtimeChecker.check(stat.mtime);

if (!isMtimeOurs) {
return setLockAsCompromised(
Expand All @@ -123,7 +137,7 @@ function updateLock(file, options) {
));
}

const mtime = new Date();
const mtime = lock.mtimeChecker.getDate();

options.fs.utimes(getLockFile(file, options), mtime, mtime, (err) => {
const isOverThreshold = lock.lastUpdate + options.stale < Date.now();
Expand All @@ -146,7 +160,6 @@ function updateLock(file, options) {
}

// All ok, keep updating..
lock.mtime = mtime;
lock.lastUpdate = Date.now();
lock.updateDelay = null;
updateLock(file, options);
Expand Down Expand Up @@ -215,7 +228,7 @@ function lock(file, options, callback) {
const operation = retry.operation(options.retries);

operation.attempt(() => {
acquireLock(file, options, (err, stat) => {
acquireLock(file, options, (err, mtimeChecker) => {
if (operation.retry(err)) {
return;
}
Expand All @@ -226,10 +239,9 @@ function lock(file, options, callback) {

// We now own the lock
const lock = locks[file] = {
mtime: stat.mtime,
mtimeChecker,
options,
lastUpdate: Date.now(),
mtimeChecker: createMtimeChecker(),
};

// We must keep the lock fresh to avoid staleness
Expand Down Expand Up @@ -310,29 +322,6 @@ function getLocks() {
return locks;
}

function createMtimeChecker() {
let precision;

return (lockMtime, statMtime) => {
// If lock time was not on the second we can determine precision
if (!precision && lockMtime % 1000 !== 0) {
precision = statMtime % 1000 === 0 ? 's' : 'ms';
}

if (precision === 's') {
const lockTs = lockMtime.getTime();
const statTs = statMtime.getTime();

// Maybe the file system truncates or rounds...
return Math.trunc(lockTs / 1000) === Math.trunc(statTs / 1000) ||
Math.round(lockTs / 1000) === Math.round(statTs / 1000);
}

// Must be ms or lockMtime was on the second
return lockMtime.getTime() === statMtime.getTime();
};
}

// Remove acquired locks on exit
/* istanbul ignore next */
onExit(() => {
Expand Down
51 changes: 51 additions & 0 deletions lib/mtime-checker.js
@@ -0,0 +1,51 @@
'use strict';

function getMtimeToProbe() {
const now = Date.now();

return new Date(now % 1000 === 0 ? now + 1 : now);
}

function maybeTruncateMtime(mtime, precision) {
if (precision === 'ms') {
return mtime;
}

const mtimeMs = Math.trunc(mtime.getTime() / 1000) * 1000;

return new Date(mtimeMs);
}

function calculatePrecision(probedMtime) {
return probedMtime.getTime() % 1000 === 0 ? 's' : 'ms';
}

function createMtimeChecker() {
let precision;
let mtime;

return {
check(statMtime) {
if (!precision) {
precision = calculatePrecision(statMtime);
}

console.log('precision', precision, mtime.getTime(), statMtime.getTime());

return maybeTruncateMtime(mtime, precision).getTime() === maybeTruncateMtime(statMtime, precision).getTime();
},
getDate() {
// If we already calculated the precision, return Date.now() according to it
if (precision) {
mtime = maybeTruncateMtime(new Date(), precision);
// Otherwise, return "not on the second" Date.now() in ms in order to probe
} else {
mtime = getMtimeToProbe();
}

return mtime;
},
};
}

module.exports = createMtimeChecker;

0 comments on commit 20f5a8c

Please sign in to comment.