Skip to content

Commit 9cc0973

Browse files
authored
feat: make staleness check more robust (#74)
BREAKING CHANGE: We were marking the lock as compromised when system went into sleep or if the event loop was busy taking too long to run the internals timers, Now we keep track of the mtime updated by the current process, and if we lose some cycles in the update process but recover and the mtime is still ours we do not mark the lock as compromised. closes #71 ref: ipfs/js-ipfs-repo#188 (comment)
1 parent a8432bc commit 9cc0973

5 files changed

Lines changed: 95 additions & 62 deletions

File tree

.travis.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
language: node_js
22
node_js:
3-
- "node"
3+
# nodejs 11.11.X was failing with this https://travis-ci.org/moxystudio/node-proper-lockfile/jobs/503161746#L469
4+
# - "node"
45
- "lts/*"
56
# Report coverage
67
after_success:

lib/lockfile.js

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ function acquireLock(file, options, callback) {
2626
options.fs.mkdir(getLockFile(file, options), (err) => {
2727
// If successful, we are done
2828
if (!err) {
29-
return callback();
29+
return options.fs.stat(getLockFile(file, options), callback);
3030
}
3131

3232
// If error is not EEXIST then some other error occurred while locking
@@ -93,47 +93,64 @@ function updateLock(file, options) {
9393

9494
lock.updateDelay = lock.updateDelay || options.update;
9595
lock.updateTimeout = setTimeout(() => {
96-
const mtime = Date.now() / 1000;
97-
9896
lock.updateTimeout = null;
9997

100-
options.fs.utimes(getLockFile(file, options), mtime, mtime, (err) => {
101-
// Ignore if the lock was released
102-
if (lock.released) {
103-
return;
104-
}
105-
106-
// Verify if we are within the stale threshold
107-
if (lock.lastUpdate <= Date.now() - options.stale && lock.lastUpdate > Date.now() - (options.stale * 2)) {
108-
const err = Object.assign(
109-
new Error(lock.updateError || 'Unable to update lock within the stale threshold'),
110-
{ code: 'ECOMPROMISED' }
111-
);
112-
113-
return setLockAsCompromised(file, lock, err);
114-
}
115-
116-
// If the file is older than (stale * 2), we assume the clock is moved manually,
117-
// which we consider a valid case
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) => {
100+
const isOverThreshold = lock.lastUpdate + options.stale < Date.now();
118101

119102
// If it failed to update the lockfile, keep trying unless
120-
// the lockfile was deleted!
103+
// the lockfile was deleted or we are over the threshold
121104
if (err) {
122-
if (err.code === 'ENOENT') {
105+
if (err.code === 'ENOENT' || isOverThreshold) {
123106
return setLockAsCompromised(file, lock, Object.assign(err, { code: 'ECOMPROMISED' }));
124107
}
125108

126-
lock.updateError = err;
127109
lock.updateDelay = 1000;
128110

129111
return updateLock(file, options);
130112
}
131113

132-
// All ok, keep updating..
133-
lock.lastUpdate = Date.now();
134-
lock.updateError = null;
135-
lock.updateDelay = null;
136-
updateLock(file, options);
114+
const isMtimeOurs = lock.mtime.getTime() === stat.mtime.getTime();
115+
116+
if (!isMtimeOurs) {
117+
return setLockAsCompromised(
118+
file,
119+
lock,
120+
Object.assign(
121+
new Error('Unable to update lock within the stale threshold'),
122+
{ code: 'ECOMPROMISED' }
123+
));
124+
}
125+
126+
const mtime = new Date();
127+
128+
options.fs.utimes(getLockFile(file, options), mtime, mtime, (err) => {
129+
const isOverThreshold = lock.lastUpdate + options.stale < Date.now();
130+
131+
// Ignore if the lock was released
132+
if (lock.released) {
133+
return;
134+
}
135+
136+
// If it failed to update the lockfile, keep trying unless
137+
// the lockfile was deleted or we are over the threshold
138+
if (err) {
139+
if (err.code === 'ENOENT' || isOverThreshold) {
140+
return setLockAsCompromised(file, lock, Object.assign(err, { code: 'ECOMPROMISED' }));
141+
}
142+
143+
lock.updateDelay = 1000;
144+
145+
return updateLock(file, options);
146+
}
147+
148+
// All ok, keep updating..
149+
lock.mtime = mtime;
150+
lock.lastUpdate = Date.now();
151+
lock.updateDelay = null;
152+
updateLock(file, options);
153+
});
137154
});
138155
}, lock.updateDelay);
139156

@@ -198,7 +215,7 @@ function lock(file, options, callback) {
198215
const operation = retry.operation(options.retries);
199216

200217
operation.attempt(() => {
201-
acquireLock(file, options, (err) => {
218+
acquireLock(file, options, (err, stat) => {
202219
if (operation.retry(err)) {
203220
return;
204221
}
@@ -209,6 +226,7 @@ function lock(file, options, callback) {
209226

210227
// We now own the lock
211228
const lock = locks[file] = {
229+
mtime: stat.mtime,
212230
options,
213231
lastUpdate: Date.now(),
214232
};

package-lock.json

Lines changed: 19 additions & 27 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"p-defer": "^1.0.0",
7070
"rimraf": "^2.6.2",
7171
"stable": "^0.1.8",
72-
"standard-version": "^5.0.0"
72+
"standard-version": "^5.0.0",
73+
"thread-sleep": "^2.1.0"
7374
}
7475
}

test/lock.test.js

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const fs = require('graceful-fs');
44
const mkdirp = require('mkdirp');
5+
const sleep = require('thread-sleep');
56
const rimraf = require('rimraf');
67
const execa = require('execa');
78
const pDefer = require('p-defer');
@@ -183,12 +184,16 @@ it('should remove and acquire over stale locks', async () => {
183184

184185
it('should retry if the lockfile was removed when verifying staleness', async () => {
185186
const mtime = (Date.now() - 60000) / 1000;
187+
let count = 0;
186188
const customFs = {
187189
...fs,
188190
mkdir: jest.fn((...args) => fs.mkdir(...args)),
189191
stat: jest.fn((...args) => {
190-
rimraf.sync(`${tmpDir}/foo.lock`);
192+
if (count % 2 === 0) {
193+
rimraf.sync(`${tmpDir}/foo.lock`);
194+
}
191195
fs.stat(...args);
196+
count += 1;
192197
}),
193198
};
194199

@@ -199,7 +204,7 @@ it('should retry if the lockfile was removed when verifying staleness', async ()
199204
await lockfile.lock(`${tmpDir}/foo`, { fs: customFs });
200205

201206
expect(customFs.mkdir).toHaveBeenCalledTimes(2);
202-
expect(customFs.stat).toHaveBeenCalledTimes(1);
207+
expect(customFs.stat).toHaveBeenCalledTimes(2);
203208
expect(fs.statSync(`${tmpDir}/foo.lock`).mtime.getTime()).toBeGreaterThan(Date.now() - 3000);
204209
});
205210

@@ -394,7 +399,7 @@ it('should call the compromised function if updating the lockfile took too much
394399

395400
const handleCompromised = (err) => {
396401
expect(err.code).toBe('ECOMPROMISED');
397-
expect(err.message).toMatch('threshold');
402+
expect(err.message).toMatch('foo');
398403

399404
deferred.resolve();
400405
};
@@ -499,3 +504,19 @@ it('should set update to a maximum of stale / 2', async () => {
499504

500505
expect(fs.statSync(`${tmpDir}/foo.lock`).mtime.getTime()).toBeGreaterThan(mtime);
501506
}, 10000);
507+
508+
it('should not fail to update mtime when we are over the threshold but mtime is ours, first', async () => {
509+
fs.writeFileSync(`${tmpDir}/foo`, '');
510+
await lockfile.lock(`${tmpDir}/foo`, { update: 1000, stale: 2000 });
511+
sleep(3000);
512+
await pDelay(5000);
513+
}, 16000);
514+
515+
it('should not fail to update mtime when we are over the threshold but mtime is ours, not first', async () => {
516+
fs.writeFileSync(`${tmpDir}/foo`, '');
517+
await lockfile.lock(`${tmpDir}/foo`, { update: 1000, stale: 2000 });
518+
setTimeout(() => {
519+
sleep(3000);
520+
}, 1000);
521+
await pDelay(5000);
522+
}, 16000);

0 commit comments

Comments
 (0)