Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: permit waking async interval with unreliable clocks #2551

Merged
merged 1 commit into from
Sep 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1036,6 +1036,9 @@ export interface InterruptableAsyncIntervalOptions {
minInterval: number;
/** Whether the method should be called immediately when the interval is started */
immediate: boolean;

/* @internal only used for testing unreliable timer environments */
clock: () => number;
}

/** @internal */
Expand Down Expand Up @@ -1066,12 +1069,13 @@ export function makeInterruptableAsyncInterval(
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 @@ -1090,6 +1094,14 @@ export function makeInterruptableAsyncInterval(
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 @@ -1114,7 +1126,7 @@ export function makeInterruptableAsyncInterval(

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

fn(err => {
if (err) throw err;
Expand All @@ -1125,7 +1137,7 @@ export function makeInterruptableAsyncInterval(
if (immediate) {
executeAndReschedule();
} else {
lastCallTime = now();
lastCallTime = clock();
reschedule(undefined);
}

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 @@ -114,5 +114,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);
});
});
});