Skip to content

Commit f266158

Browse files
authored
fix: fix mtime precision on some filesystems (#88)
Closes #82, #87
1 parent c0cdea2 commit f266158

5 files changed

Lines changed: 198 additions & 110 deletions

File tree

lib/lockfile.js

Lines changed: 28 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const path = require('path');
44
const fs = require('graceful-fs');
55
const retry = require('retry');
66
const onExit = require('signal-exit');
7+
const mtimePrecision = require('./mtime-precision');
78

89
const locks = {};
910

@@ -22,11 +23,24 @@ function resolveCanonicalPath(file, options, callback) {
2223
}
2324

2425
function acquireLock(file, options, callback) {
26+
const lockfilePath = getLockFile(file, options);
27+
2528
// Use mkdir to create the lockfile (atomic operation)
26-
options.fs.mkdir(getLockFile(file, options), (err) => {
27-
// If successful, we are done
29+
options.fs.mkdir(lockfilePath, (err) => {
2830
if (!err) {
29-
return options.fs.stat(getLockFile(file, options), callback);
31+
// At this point, we acquired the lock!
32+
// Probe the mtime precision
33+
return mtimePrecision.probe(lockfilePath, options.fs, (err, mtime, mtimePrecision) => {
34+
// If it failed, try to remove the lock..
35+
/* istanbul ignore if */
36+
if (err) {
37+
options.fs.rmdir(lockfilePath, () => {});
38+
39+
return callback(err);
40+
}
41+
42+
callback(null, mtime, mtimePrecision);
43+
});
3044
}
3145

3246
// If error is not EEXIST then some other error occurred while locking
@@ -39,7 +53,7 @@ function acquireLock(file, options, callback) {
3953
return callback(Object.assign(new Error('Lock file is already being held'), { code: 'ELOCKED', file }));
4054
}
4155

42-
options.fs.stat(getLockFile(file, options), (err, stat) => {
56+
options.fs.stat(lockfilePath, (err, stat) => {
4357
if (err) {
4458
// Retry if the lockfile has been removed (meanwhile)
4559
// Skip stale check to avoid recursiveness
@@ -95,8 +109,9 @@ function updateLock(file, options) {
95109
lock.updateTimeout = setTimeout(() => {
96110
lock.updateTimeout = null;
97111

98-
// Check if mtime is still ours if it is we can still recover from a system sleep or a busy event loop
99-
options.fs.stat(getLockFile(file, options), (err, stat) => {
112+
// Stat the file to check if mtime is still ours
113+
// If it is, we can still recover from a system sleep or a busy event loop
114+
options.fs.stat(lock.lockfilePath, (err, stat) => {
100115
const isOverThreshold = lock.lastUpdate + options.stale < Date.now();
101116

102117
// If it failed to update the lockfile, keep trying unless
@@ -111,7 +126,7 @@ function updateLock(file, options) {
111126
return updateLock(file, options);
112127
}
113128

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

116131
if (!isMtimeOurs) {
117132
return setLockAsCompromised(
@@ -123,9 +138,9 @@ function updateLock(file, options) {
123138
));
124139
}
125140

126-
const mtime = new Date();
141+
const mtime = mtimePrecision.getMtime(lock.mtimePrecision);
127142

128-
options.fs.utimes(getLockFile(file, options), mtime, mtime, (err) => {
143+
options.fs.utimes(lock.lockfilePath, mtime, mtime, (err) => {
129144
const isOverThreshold = lock.lastUpdate + options.stale < Date.now();
130145

131146
// Ignore if the lock was released
@@ -215,7 +230,7 @@ function lock(file, options, callback) {
215230
const operation = retry.operation(options.retries);
216231

217232
operation.attempt(() => {
218-
acquireLock(file, options, (err, stat) => {
233+
acquireLock(file, options, (err, mtime, mtimePrecision) => {
219234
if (operation.retry(err)) {
220235
return;
221236
}
@@ -226,10 +241,11 @@ function lock(file, options, callback) {
226241

227242
// We now own the lock
228243
const lock = locks[file] = {
229-
mtime: stat.mtime,
244+
lockfilePath: getLockFile(file, options),
245+
mtime,
246+
mtimePrecision,
230247
options,
231248
lastUpdate: Date.now(),
232-
mtimeChecker: createMtimeChecker(),
233249
};
234250

235251
// We must keep the lock fresh to avoid staleness
@@ -310,29 +326,6 @@ function getLocks() {
310326
return locks;
311327
}
312328

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-
336329
// Remove acquired locks on exit
337330
/* istanbul ignore next */
338331
onExit(() => {

lib/mtime-precision.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
'use strict';
2+
3+
const cacheSymbol = Symbol();
4+
5+
function probe(file, fs, callback) {
6+
const cachedPrecision = fs[cacheSymbol];
7+
8+
if (cachedPrecision) {
9+
return fs.stat(file, (err, stat) => {
10+
/* istanbul ignore if */
11+
if (err) {
12+
return callback(err);
13+
}
14+
15+
callback(null, stat.mtime, cachedPrecision);
16+
});
17+
}
18+
19+
// Set mtime by ceiling Date.now() to seconds + 5ms so that it's "not on the second"
20+
const mtime = new Date((Math.ceil(Date.now() / 1000) * 1000) + 5);
21+
22+
fs.utimes(file, mtime, mtime, (err) => {
23+
/* istanbul ignore if */
24+
if (err) {
25+
return callback(err);
26+
}
27+
28+
fs.stat(file, (err, stat) => {
29+
/* istanbul ignore if */
30+
if (err) {
31+
return callback(err);
32+
}
33+
34+
const precision = stat.mtime.getTime() % 1000 === 0 ? 's' : 'ms';
35+
36+
// Cache the precision in a non-enumerable way
37+
Object.defineProperty(fs, cacheSymbol, { value: precision });
38+
39+
callback(null, stat.mtime, precision);
40+
});
41+
});
42+
}
43+
44+
function getMtime(precision) {
45+
let now = Date.now();
46+
47+
if (precision === 's') {
48+
now = Math.ceil(now / 1000) * 1000;
49+
}
50+
51+
return new Date(now);
52+
}
53+
54+
module.exports.probe = probe;
55+
module.exports.getMtime = getMtime;

test/check.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ it('should not resolve symlinks if realpath is false', async () => {
108108
it('should fail if stating the lockfile errors out when verifying staleness', async () => {
109109
fs.writeFileSync(`${tmpDir}/foo`, '');
110110

111-
const mtime = (Date.now() - 60000) / 1000;
111+
const mtime = new Date(Date.now() - 60000);
112112
const customFs = {
113113
...fs,
114114
stat: (path, callback) => callback(new Error('foo')),

0 commit comments

Comments
 (0)