diff --git a/lib/utils.js b/lib/utils.js index cc5ce38aee..71548eb5a9 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -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 @@ -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() { @@ -758,7 +767,7 @@ function makeInterruptableAsyncInterval(fn, options) { function executeAndReschedule() { lastWakeTime = 0; - lastCallTime = now(); + lastCallTime = clock(); fn(err => { if (err) throw err; @@ -769,7 +778,7 @@ function makeInterruptableAsyncInterval(fn, options) { if (immediate) { executeAndReschedule(); } else { - lastCallTime = now(); + lastCallTime = clock(); reschedule(); } diff --git a/test/unit/utils.test.js b/test/unit/utils.test.js index c8dd71e918..23193764a9 100644 --- a/test/unit/utils.test.js +++ b/test/unit/utils.test.js @@ -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); + }); }); });