Skip to content

Commit

Permalink
fix: permit waking async interval with unreliable clock
Browse files Browse the repository at this point in the history
The logic for waking the AsyncInterruptibleInterval sooner than its
interval is dependent on an ability to reliably mark the last call
made to the wrapped function. In environments like AWS Lambda where
instances can be frozen and later thawed, it's possible for the
last call to be in a distant past even though the internal timer
has not completed yet. This change ensures that we immediately
reschedule in these situations.

NODE-2829
  • Loading branch information
mbroadst committed Sep 29, 2020
1 parent 6113b24 commit e0e11bb
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 4 deletions.
17 changes: 13 additions & 4 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -713,12 +713,13 @@ function makeInterruptableAsyncInterval(fn, options) {
const interval = options.interval || 1000;
const minInterval = options.minInterval || 500;
const immediate = typeof options.immediate === 'boolean' ? options.immediate : false;
const clock = typeof options.clock === 'function' ? options.clock : now;

function wake() {
const currentTime = now();
const currentTime = clock();
const timeSinceLastWake = currentTime - lastWakeTime;
const timeSinceLastCall = currentTime - lastCallTime;
const timeUntilNextCall = Math.max(interval - timeSinceLastCall, 0);
const timeUntilNextCall = interval - timeSinceLastCall;
lastWakeTime = currentTime;

// For the streaming protocol: there is nothing obviously stopping this
Expand All @@ -737,6 +738,14 @@ function makeInterruptableAsyncInterval(fn, options) {
if (timeUntilNextCall > minInterval) {
reschedule(minInterval);
}

// This is possible in virtualized environments like AWS Lambda where our
// clock is unreliable. In these cases the timer is "running" but never
// actually completes, so we want to execute immediately and then attempt
// to reschedule.
if (timeUntilNextCall < 0) {
executeAndReschedule();
}
}

function stop() {
Expand All @@ -758,7 +767,7 @@ function makeInterruptableAsyncInterval(fn, options) {

function executeAndReschedule() {
lastWakeTime = 0;
lastCallTime = now();
lastCallTime = clock();

fn(err => {
if (err) throw err;
Expand All @@ -769,7 +778,7 @@ function makeInterruptableAsyncInterval(fn, options) {
if (immediate) {
executeAndReschedule();
} else {
lastCallTime = now();
lastCallTime = clock();
reschedule();
}

Expand Down
46 changes: 46 additions & 0 deletions test/unit/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,5 +116,51 @@ describe('utils', function() {

this.clock.tick(250);
});

it("should immediately schedule if the clock is unreliable", function (done) {
let clockCalled = 0;
let lastTime = now();
const marks = [];
const executor = makeInterruptableAsyncInterval(
(callback) => {
marks.push(now() - lastTime);
lastTime = now();
callback();
},
{
interval: 50,
minInterval: 10,
immediate: true,
clock() {
clockCalled += 1;

// needs to happen on the third call because `wake` checks
// the `currentTime` at the beginning of the function
if (clockCalled === 3) {
return now() - 100000;
}

return now();
},
}
);

// force mark at 20ms, and then the unreliable system clock
// will report a very stale `lastCallTime` on this mark.
setTimeout(() => executor.wake(), 10);

// try to wake again in another `minInterval + immediate`, now
// using a very old `lastCallTime`. This should result in an
// immediate scheduling: 0ms (immediate), 20ms (wake with minIterval)
// and then 10ms for another immediate.
setTimeout(() => executor.wake(), 30);

setTimeout(() => {
executor.stop();
expect(marks).to.eql([0, 20, 10, 50, 50, 50, 50]);
done();
}, 250);
this.clock.tick(250);
});
});
});

0 comments on commit e0e11bb

Please sign in to comment.