Skip to content

Commit b2816a6

Browse files
Alan Shawsatazor
authored andcommitted
feat: allow second precision in mtime comparison (#78)
1 parent 4375941 commit b2816a6

File tree

2 files changed

+105
-1
lines changed

2 files changed

+105
-1
lines changed

lib/lockfile.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ function updateLock(file, options) {
111111
return updateLock(file, options);
112112
}
113113

114-
const isMtimeOurs = lock.mtime.getTime() === stat.mtime.getTime();
114+
const isMtimeOurs = lock.mtimeChecker(lock.mtime, stat.mtime);
115115

116116
if (!isMtimeOurs) {
117117
return setLockAsCompromised(
@@ -229,6 +229,7 @@ function lock(file, options, callback) {
229229
mtime: stat.mtime,
230230
options,
231231
lastUpdate: Date.now(),
232+
mtimeChecker: createMtimeChecker(),
232233
};
233234

234235
// We must keep the lock fresh to avoid staleness
@@ -309,6 +310,29 @@ function getLocks() {
309310
return locks;
310311
}
311312

313+
function createMtimeChecker() {
314+
let precision;
315+
316+
return (lockMtime, statMtime) => {
317+
// If lock time was not on the second we can determine precision
318+
if (!precision && lockMtime % 1000 !== 0) {
319+
precision = statMtime % 1000 === 0 ? 's' : 'ms';
320+
}
321+
322+
if (precision === 's') {
323+
const lockTs = lockMtime.getTime();
324+
const statTs = statMtime.getTime();
325+
326+
// Maybe the file system truncates or rounds...
327+
return Math.trunc(lockTs / 1000) === Math.trunc(statTs / 1000) ||
328+
Math.round(lockTs / 1000) === Math.round(statTs / 1000);
329+
}
330+
331+
// Must be ms or lockMtime was on the second
332+
return lockMtime.getTime() === statMtime.getTime();
333+
};
334+
}
335+
312336
// Remove acquired locks on exit
313337
/* istanbul ignore next */
314338
onExit(() => {

test/lock.test.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,3 +520,83 @@ it('should not fail to update mtime when we are over the threshold but mtime is
520520
}, 1000);
521521
await pDelay(5000);
522522
}, 16000);
523+
524+
it('should allow truncated second precision mtime', async () => {
525+
fs.writeFileSync(`${tmpDir}/foo`, '');
526+
527+
const customFs = {
528+
...fs,
529+
stat(path, cb) {
530+
fs.stat(path, (err, stat) => {
531+
if (err) {
532+
return cb(err);
533+
}
534+
535+
// Make second precision if not already
536+
stat.mtime = new Date(Math.trunc(stat.mtime.getTime() / 1000) * 1000);
537+
cb(null, stat);
538+
});
539+
},
540+
};
541+
542+
const deferred = pDefer();
543+
544+
// If the module does not detect second precision for mtime, the mtime
545+
// comparison will cause the lock to appear compromised.
546+
const handleCompromised = (err) => {
547+
clearTimeout(successTimeoutId);
548+
deferred.reject(err);
549+
};
550+
551+
await lockfile.lock(`${tmpDir}/foo`, {
552+
fs: customFs,
553+
update: 1000,
554+
onCompromised: handleCompromised,
555+
});
556+
557+
// First update is fine because we `stat` after we `mkdir`, it'll fail on
558+
// the second update when we use `utimes` and do not `stat` for the `mtime`
559+
const successTimeoutId = setTimeout(deferred.resolve, 3000);
560+
561+
await deferred.promise;
562+
});
563+
564+
it('should allow rounded second precision mtime', async () => {
565+
fs.writeFileSync(`${tmpDir}/foo`, '');
566+
567+
const customFs = {
568+
...fs,
569+
stat(path, cb) {
570+
fs.stat(path, (err, stat) => {
571+
if (err) {
572+
return cb(err);
573+
}
574+
575+
// Make second precision if not already
576+
stat.mtime = new Date(Math.round(stat.mtime.getTime() / 1000) * 1000);
577+
cb(null, stat);
578+
});
579+
},
580+
};
581+
582+
const deferred = pDefer();
583+
584+
// If the module does not detect second precision for mtime, the mtime
585+
// comparison will cause the lock to appear compromised.
586+
const handleCompromised = (err) => {
587+
clearTimeout(successTimeoutId);
588+
deferred.reject(err);
589+
};
590+
591+
await lockfile.lock(`${tmpDir}/foo`, {
592+
fs: customFs,
593+
update: 1000,
594+
onCompromised: handleCompromised,
595+
});
596+
597+
// First update is fine because we `stat` after we `mkdir`, it'll fail on
598+
// the second update when we use `utimes` and do not `stat` for the `mtime`
599+
const successTimeoutId = setTimeout(deferred.resolve, 3000);
600+
601+
await deferred.promise;
602+
});

0 commit comments

Comments
 (0)