Skip to content

Commit 460cc62

Browse files
apapirovskiBridgeAR
authored andcommitted
process: code cleanup for nextTick
Fix a few edge cases and non-obvious issues with nextTick: 1. Emit destroy hook in a try-finally rather than triggering it before the callback runs. 2. Re-word comment for processPromiseRejections and make sure it returns true in the rejectionHandled case too. 3. Small readability improvements. PR-URL: #28047 Reviewed-By: Ruben Bridgewater <[email protected]> Reviewed-By: Joyee Cheung <[email protected]> Reviewed-By: Jeremiah Senkpiel <[email protected]> Reviewed-By: Rich Trott <[email protected]>
1 parent d4bb88e commit 460cc62

File tree

3 files changed

+73
-29
lines changed

3 files changed

+73
-29
lines changed

lib/internal/process/promises.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -139,17 +139,19 @@ function emitDeprecationWarning() {
139139
}
140140
}
141141

142-
// If this method returns true, at least one more tick need to be
143-
// scheduled to process any potential pending rejections
142+
// If this method returns true, we've executed user code or triggered
143+
// a warning to be emitted which requires the microtask and next tick
144+
// queues to be drained again.
144145
function processPromiseRejections() {
146+
let maybeScheduledTicksOrMicrotasks = asyncHandledRejections.length > 0;
147+
145148
while (asyncHandledRejections.length > 0) {
146149
const { promise, warning } = asyncHandledRejections.shift();
147150
if (!process.emit('rejectionHandled', promise)) {
148151
process.emitWarning(warning);
149152
}
150153
}
151154

152-
let maybeScheduledTicks = false;
153155
let len = pendingUnhandledRejections.length;
154156
while (len--) {
155157
const promise = pendingUnhandledRejections.shift();
@@ -167,9 +169,10 @@ function processPromiseRejections() {
167169
state === states.warn) {
168170
emitWarning(uid, reason);
169171
}
170-
maybeScheduledTicks = true;
172+
maybeScheduledTicksOrMicrotasks = true;
171173
}
172-
return maybeScheduledTicks || pendingUnhandledRejections.length !== 0;
174+
return maybeScheduledTicksOrMicrotasks ||
175+
pendingUnhandledRejections.length !== 0;
173176
}
174177

175178
function getError(name, message) {

lib/internal/process/task_queues.js

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -65,46 +65,38 @@ function processTicksAndRejections() {
6565
while (tock = queue.shift()) {
6666
const asyncId = tock[async_id_symbol];
6767
emitBefore(asyncId, tock[trigger_async_id_symbol]);
68-
// emitDestroy() places the async_id_symbol into an asynchronous queue
69-
// that calls the destroy callback in the future. It's called before
70-
// calling tock.callback so destroy will be called even if the callback
71-
// throws an exception that is handled by 'uncaughtException' or a
72-
// domain.
73-
// TODO(trevnorris): This is a bit of a hack. It relies on the fact
74-
// that nextTick() doesn't allow the event loop to proceed, but if
75-
// any async hooks are enabled during the callback's execution then
76-
// this tock's after hook will be called, but not its destroy hook.
77-
if (destroyHooksExist())
78-
emitDestroy(asyncId);
79-
80-
const callback = tock.callback;
81-
if (tock.args === undefined)
82-
callback();
83-
else
84-
callback(...tock.args);
68+
69+
try {
70+
const callback = tock.callback;
71+
if (tock.args === undefined)
72+
callback();
73+
else
74+
callback(...tock.args);
75+
} finally {
76+
if (destroyHooksExist())
77+
emitDestroy(asyncId);
78+
}
8579

8680
emitAfter(asyncId);
8781
}
88-
setHasTickScheduled(false);
8982
runMicrotasks();
9083
} while (!queue.isEmpty() || processPromiseRejections());
84+
setHasTickScheduled(false);
9185
setHasRejectionToWarn(false);
9286
}
9387

9488
class TickObject {
95-
constructor(callback, args, triggerAsyncId) {
89+
constructor(callback, args) {
9690
this.callback = callback;
9791
this.args = args;
9892

9993
const asyncId = newAsyncId();
94+
const triggerAsyncId = getDefaultTriggerAsyncId();
10095
this[async_id_symbol] = asyncId;
10196
this[trigger_async_id_symbol] = triggerAsyncId;
10297

10398
if (initHooksExist()) {
104-
emitInit(asyncId,
105-
'TickObject',
106-
triggerAsyncId,
107-
this);
99+
emitInit(asyncId, 'TickObject', triggerAsyncId, this);
108100
}
109101
}
110102
}
@@ -132,7 +124,7 @@ function nextTick(callback) {
132124

133125
if (queue.isEmpty())
134126
setHasTickScheduled(true);
135-
queue.push(new TickObject(callback, args, getDefaultTriggerAsyncId()));
127+
queue.push(new TickObject(callback, args));
136128
}
137129

138130
let AsyncResource;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const async_hooks = require('async_hooks');
5+
6+
// Checks that enabling async hooks in a callback actually
7+
// triggers after & destroy as expected.
8+
9+
const fnsToTest = [setTimeout, (cb) => {
10+
setImmediate(() => {
11+
cb();
12+
13+
// We need to keep the event loop open for this to actually work
14+
// since destroy hooks are triggered in unrefed Immediates
15+
setImmediate(() => {
16+
hook.disable();
17+
});
18+
});
19+
}, (cb) => {
20+
process.nextTick(() => {
21+
cb();
22+
23+
// We need to keep the event loop open for this to actually work
24+
// since destroy hooks are triggered in unrefed Immediates
25+
setImmediate(() => {
26+
hook.disable();
27+
assert.strictEqual(fnsToTest.length, 0);
28+
});
29+
});
30+
}];
31+
32+
const hook = async_hooks.createHook({
33+
before: common.mustNotCall(),
34+
after: common.mustCall(() => {}, 3),
35+
destroy: common.mustCall(() => {
36+
hook.disable();
37+
nextTest();
38+
}, 3)
39+
});
40+
41+
nextTest();
42+
43+
function nextTest() {
44+
if (fnsToTest.length > 0) {
45+
fnsToTest.shift()(common.mustCall(() => {
46+
hook.enable();
47+
}));
48+
}
49+
}

0 commit comments

Comments
 (0)