Skip to content

Commit a2c113f

Browse files
authored
fix: permit waking async interval with unreliable clock (#2551)
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
1 parent ee8ca1a commit a2c113f

File tree

2 files changed

+62
-4
lines changed

2 files changed

+62
-4
lines changed

src/utils.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,6 +1036,9 @@ export interface InterruptableAsyncIntervalOptions {
10361036
minInterval: number;
10371037
/** Whether the method should be called immediately when the interval is started */
10381038
immediate: boolean;
1039+
1040+
/* @internal only used for testing unreliable timer environments */
1041+
clock: () => number;
10391042
}
10401043

10411044
/** @internal */
@@ -1066,12 +1069,13 @@ export function makeInterruptableAsyncInterval(
10661069
const interval = options.interval || 1000;
10671070
const minInterval = options.minInterval || 500;
10681071
const immediate = typeof options.immediate === 'boolean' ? options.immediate : false;
1072+
const clock = typeof options.clock === 'function' ? options.clock : now;
10691073

10701074
function wake() {
1071-
const currentTime = now();
1075+
const currentTime = clock();
10721076
const timeSinceLastWake = currentTime - lastWakeTime;
10731077
const timeSinceLastCall = currentTime - lastCallTime;
1074-
const timeUntilNextCall = Math.max(interval - timeSinceLastCall, 0);
1078+
const timeUntilNextCall = interval - timeSinceLastCall;
10751079
lastWakeTime = currentTime;
10761080

10771081
// For the streaming protocol: there is nothing obviously stopping this
@@ -1090,6 +1094,14 @@ export function makeInterruptableAsyncInterval(
10901094
if (timeUntilNextCall > minInterval) {
10911095
reschedule(minInterval);
10921096
}
1097+
1098+
// This is possible in virtualized environments like AWS Lambda where our
1099+
// clock is unreliable. In these cases the timer is "running" but never
1100+
// actually completes, so we want to execute immediately and then attempt
1101+
// to reschedule.
1102+
if (timeUntilNextCall < 0) {
1103+
executeAndReschedule();
1104+
}
10931105
}
10941106

10951107
function stop() {
@@ -1114,7 +1126,7 @@ export function makeInterruptableAsyncInterval(
11141126

11151127
function executeAndReschedule() {
11161128
lastWakeTime = 0;
1117-
lastCallTime = now();
1129+
lastCallTime = clock();
11181130

11191131
fn(err => {
11201132
if (err) throw err;
@@ -1125,7 +1137,7 @@ export function makeInterruptableAsyncInterval(
11251137
if (immediate) {
11261138
executeAndReschedule();
11271139
} else {
1128-
lastCallTime = now();
1140+
lastCallTime = clock();
11291141
reschedule(undefined);
11301142
}
11311143

test/unit/utils.test.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,5 +114,51 @@ describe('utils', function () {
114114

115115
this.clock.tick(250);
116116
});
117+
118+
it('should immediately schedule if the clock is unreliable', function (done) {
119+
let clockCalled = 0;
120+
let lastTime = now();
121+
const marks = [];
122+
const executor = makeInterruptableAsyncInterval(
123+
callback => {
124+
marks.push(now() - lastTime);
125+
lastTime = now();
126+
callback();
127+
},
128+
{
129+
interval: 50,
130+
minInterval: 10,
131+
immediate: true,
132+
clock() {
133+
clockCalled += 1;
134+
135+
// needs to happen on the third call because `wake` checks
136+
// the `currentTime` at the beginning of the function
137+
if (clockCalled === 3) {
138+
return now() - 100000;
139+
}
140+
141+
return now();
142+
}
143+
}
144+
);
145+
146+
// force mark at 20ms, and then the unreliable system clock
147+
// will report a very stale `lastCallTime` on this mark.
148+
setTimeout(() => executor.wake(), 10);
149+
150+
// try to wake again in another `minInterval + immediate`, now
151+
// using a very old `lastCallTime`. This should result in an
152+
// immediate scheduling: 0ms (immediate), 20ms (wake with minIterval)
153+
// and then 10ms for another immediate.
154+
setTimeout(() => executor.wake(), 30);
155+
156+
setTimeout(() => {
157+
executor.stop();
158+
expect(marks).to.eql([0, 20, 10, 50, 50, 50, 50]);
159+
done();
160+
}, 250);
161+
this.clock.tick(250);
162+
});
117163
});
118164
});

0 commit comments

Comments
 (0)