Skip to content

Commit 0e15cc6

Browse files
committed
lib: run microtasks before ticks
This resolve multiple timing issues related to promises and nextTick. As well as resolving zaldo in promise only code, i.e. our current best practice of using process.nextTick will always apply and work. Refs: #51156 Refs: #51156 (comment) Refs: #51114 Refs: #51070 Refs: #51156 PR-URL: #51267
1 parent ee61c2c commit 0e15cc6

File tree

2 files changed

+74
-5
lines changed

2 files changed

+74
-5
lines changed

doc/api/cli.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -863,6 +863,20 @@ Use this flag to generate a blob that can be injected into the Node.js
863863
binary to produce a [single executable application][]. See the documentation
864864
about [this configuration][`--experimental-sea-config`] for details.
865865

866+
867+
### `--experimental-task-ordering`
868+
869+
<!-- YAML
870+
added: REPLACEME
871+
-->
872+
873+
> Stability: 1 - Experimental
874+
875+
Enable experimental task ordering. Always drain micro task queue
876+
before running `process.nextTick` to avoid unintuitive behavior
877+
and unexpected logical deadlocks when mixing async callback and
878+
event API's with `Promise`, `async`/`await`` and `queueMicroTask`.
879+
866880
### `--experimental-shadow-realm`
867881

868882
<!-- YAML

lib/internal/process/task_queues.js

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ const {
4040
} = require('internal/validators');
4141

4242
const { AsyncResource } = require('async_hooks');
43+
const { run } = require('internal/test_runner/runner');
4344

45+
let experimentalTaskOrdering;
4446
// *Must* match Environment::TickInfo::Fields in src/env.h.
4547
const kHasTickScheduled = 0;
4648

@@ -55,16 +57,25 @@ function setHasTickScheduled(value) {
5557
const queue = new FixedQueue();
5658

5759
// Should be in sync with RunNextTicksNative in node_task_queue.cc
58-
function runNextTicks() {
60+
function runNextTicksNew() {
5961
if (!hasTickScheduled() && !hasRejectionToWarn())
6062
runMicrotasks();
6163
if (!hasTickScheduled() && !hasRejectionToWarn())
6264
return;
6365

64-
processTicksAndRejections();
66+
processTicksAndRejectionsNew();
6567
}
6668

67-
function processTicksAndRejections() {
69+
function runNextTicksOld() {
70+
if (!hasTickScheduled() && !hasRejectionToWarn())
71+
runMicrotasks();
72+
if (!hasTickScheduled() && !hasRejectionToWarn())
73+
return;
74+
75+
processTicksAndRejectionsOld();
76+
}
77+
78+
function processTicksAndRejectionsOld() {
6879
let tock;
6980
do {
7081
while ((tock = queue.shift()) !== null) {
@@ -98,6 +109,40 @@ function processTicksAndRejections() {
98109
setHasRejectionToWarn(false);
99110
}
100111

112+
function processTicksAndRejectionsNew() {
113+
let tock;
114+
do {
115+
runMicrotasks();
116+
while ((tock = queue.shift()) !== null) {
117+
const asyncId = tock[async_id_symbol];
118+
emitBefore(asyncId, tock[trigger_async_id_symbol], tock);
119+
120+
try {
121+
const callback = tock.callback;
122+
if (tock.args === undefined) {
123+
callback();
124+
} else {
125+
const args = tock.args;
126+
switch (args.length) {
127+
case 1: callback(args[0]); break;
128+
case 2: callback(args[0], args[1]); break;
129+
case 3: callback(args[0], args[1], args[2]); break;
130+
case 4: callback(args[0], args[1], args[2], args[3]); break;
131+
default: callback(...args);
132+
}
133+
}
134+
} finally {
135+
if (destroyHooksExist())
136+
emitDestroy(asyncId);
137+
}
138+
139+
emitAfter(asyncId);
140+
}
141+
} while (!queue.isEmpty() || processPromiseRejections());
142+
setHasTickScheduled(false);
143+
setHasRejectionToWarn(false);
144+
}
145+
101146
// `nextTick()` will not enqueue any callback when the process is about to
102147
// exit since the callback would not have a chance to be executed.
103148
function nextTick(callback) {
@@ -160,13 +205,23 @@ function queueMicrotask(callback) {
160205

161206
module.exports = {
162207
setupTaskQueue() {
208+
if (experimentalTaskOrdering === undefined) {
209+
const { getOptionValue } = require('internal/options');
210+
experimentalTaskOrdering = getOptionValue('--experimental-task-ordering');
211+
}
212+
163213
// Sets the per-isolate promise rejection callback
164214
listenForRejections();
165215
// Sets the callback to be run in every tick.
166-
setTickCallback(processTicksAndRejections);
216+
setTickCallback(experimentalTaskOrdering
217+
? processTicksAndRejectionsNew
218+
: processTicksAndRejectionsOld
219+
);
167220
return {
168221
nextTick,
169-
runNextTicks,
222+
runNextTicks: experimentalTaskOrdering
223+
? runNextTicksNew
224+
: runNextTicksOld,
170225
};
171226
},
172227
queueMicrotask,

0 commit comments

Comments
 (0)