Skip to content

Commit 3f1e38c

Browse files
committed
timers: use consistent checks for canceled timers
Previously not all codepaths set `timer._idleTimeout = -1` for canceled or closed timers, and not all codepaths checked it either. Unenroll uses this to say that a timer is indeed closed and it is the closest thing there is to an authoritative source for this. Refs: #9606 Fixes: #9561 PR-URL: #9685 Reviewed-By: Rich Trott <[email protected]>
1 parent 929fd4a commit 3f1e38c

File tree

2 files changed

+63
-2
lines changed

2 files changed

+63
-2
lines changed

lib/timers.js

+14-2
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,9 @@ function ontimeout(timer) {
384384

385385

386386
function rearm(timer) {
387+
// // Do not re-arm unenroll'd or closed timers.
388+
if (timer._idleTimeout === -1) return;
389+
387390
// If timer is unref'd (or was - it's permanently removed from the list.)
388391
if (timer._handle && timer instanceof Timeout) {
389392
timer._handle.start(timer._repeat);
@@ -463,9 +466,17 @@ function Timeout(after, callback, args) {
463466

464467

465468
function unrefdHandle() {
466-
ontimeout(this.owner);
467-
if (!this.owner._repeat)
469+
// Don't attempt to call the callback if it is not a function.
470+
if (typeof this.owner._onTimeout === 'function') {
471+
ontimeout(this.owner);
472+
}
473+
474+
// Make sure we clean up if the callback is no longer a function
475+
// even if the timer is an interval.
476+
if (!this.owner._repeat
477+
|| typeof this.owner._onTimeout !== 'function') {
468478
this.owner.close();
479+
}
469480
}
470481

471482

@@ -505,6 +516,7 @@ Timeout.prototype.ref = function() {
505516
Timeout.prototype.close = function() {
506517
this._onTimeout = null;
507518
if (this._handle) {
519+
this._idleTimeout = -1;
508520
this._handle[kOnTimeout] = null;
509521
this._handle.close();
510522
} else {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const timers = require('timers');
5+
6+
{
7+
const interval = setInterval(common.mustCall(() => {
8+
clearTimeout(interval);
9+
}), 1).unref();
10+
}
11+
12+
{
13+
const interval = setInterval(common.mustCall(() => {
14+
interval.close();
15+
}), 1).unref();
16+
}
17+
18+
{
19+
const interval = setInterval(common.mustCall(() => {
20+
timers.unenroll(interval);
21+
}), 1).unref();
22+
}
23+
24+
{
25+
const interval = setInterval(common.mustCall(() => {
26+
interval._idleTimeout = -1;
27+
}), 1).unref();
28+
}
29+
30+
{
31+
const interval = setInterval(common.mustCall(() => {
32+
interval._onTimeout = null;
33+
}), 1).unref();
34+
}
35+
36+
// Use timers' intrinsic behavior to keep this open
37+
// exactly long enough for the problem to manifest.
38+
//
39+
// See https://github.com/nodejs/node/issues/9561
40+
//
41+
// Since this is added after it will always fire later
42+
// than the previous timeouts, unrefed or not.
43+
//
44+
// Keep the event loop alive for one timeout and then
45+
// another. Any problems will occur when the second
46+
// should be called but before it is able to be.
47+
setTimeout(common.mustCall(() => {
48+
setTimeout(common.mustCall(() => {}), 1);
49+
}), 1);

0 commit comments

Comments
 (0)