Skip to content

Commit

Permalink
lib: run microtasks before ticks
Browse files Browse the repository at this point in the history
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: nodejs#51156
Refs: nodejs#51156 (comment)
Refs: nodejs#51114
Refs: nodejs#51070
Refs: nodejs#51156

PR-URL: nodejs#51267
  • Loading branch information
ronag committed Dec 24, 2023
1 parent ee61c2c commit 0d9b9ca
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 5 deletions.
14 changes: 14 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,20 @@ Use this flag to generate a blob that can be injected into the Node.js
binary to produce a [single executable application][]. See the documentation
about [this configuration][`--experimental-sea-config`] for details.


### `--experimental-task-ordering`

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental
Enable experimental task ordering. Always drain micro task queue
before running `process.nextTick` to avoid unintuitive behavior
and unexpected logical deadlocks when mixing async callback and
event API's with `Promise`, `async`/`await`` and `queueMicroTask`.

### `--experimental-shadow-realm`

<!-- YAML
Expand Down
65 changes: 60 additions & 5 deletions lib/internal/process/task_queues.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const {

const { AsyncResource } = require('async_hooks');

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

Expand All @@ -55,16 +56,26 @@ function setHasTickScheduled(value) {
const queue = new FixedQueue();

// Should be in sync with RunNextTicksNative in node_task_queue.cc
function runNextTicks() {
function runNextTicksNew() {
if (!hasTickScheduled() && !hasRejectionToWarn())
runMicrotasks();
if (!hasTickScheduled() && !hasRejectionToWarn())
return;

processTicksAndRejections();
processTicksAndRejectionsNew();
}

function processTicksAndRejections() {
// Should be in sync with RunNextTicksNative in node_task_queue.cc
function runNextTicksOld() {
if (!hasTickScheduled() && !hasRejectionToWarn())
runMicrotasks();
if (!hasTickScheduled() && !hasRejectionToWarn())
return;

processTicksAndRejectionsOld();
}

function processTicksAndRejectionsOld() {
let tock;
do {
while ((tock = queue.shift()) !== null) {
Expand Down Expand Up @@ -98,6 +109,40 @@ function processTicksAndRejections() {
setHasRejectionToWarn(false);
}

function processTicksAndRejectionsNew() {
let tock;
do {
runMicrotasks();
while ((tock = queue.shift()) !== null) {
const asyncId = tock[async_id_symbol];
emitBefore(asyncId, tock[trigger_async_id_symbol], tock);

try {
const callback = tock.callback;
if (tock.args === undefined) {
callback();
} else {
const args = tock.args;
switch (args.length) {
case 1: callback(args[0]); break;
case 2: callback(args[0], args[1]); break;
case 3: callback(args[0], args[1], args[2]); break;
case 4: callback(args[0], args[1], args[2], args[3]); break;
default: callback(...args);
}
}
} finally {
if (destroyHooksExist())
emitDestroy(asyncId);
}

emitAfter(asyncId);
}
} while (!queue.isEmpty() || processPromiseRejections());
setHasTickScheduled(false);
setHasRejectionToWarn(false);
}

// `nextTick()` will not enqueue any callback when the process is about to
// exit since the callback would not have a chance to be executed.
function nextTick(callback) {
Expand Down Expand Up @@ -160,13 +205,23 @@ function queueMicrotask(callback) {

module.exports = {
setupTaskQueue() {
if (experimentalTaskOrdering === undefined) {
const { getOptionValue } = require('internal/options');
experimentalTaskOrdering = getOptionValue('--experimental-task-ordering');
}

// Sets the per-isolate promise rejection callback
listenForRejections();
// Sets the callback to be run in every tick.
setTickCallback(processTicksAndRejections);
setTickCallback(experimentalTaskOrdering
? processTicksAndRejectionsNew
: processTicksAndRejectionsOld
);
return {
nextTick,
runNextTicks,
runNextTicks: experimentalTaskOrdering
? runNextTicksNew
: runNextTicksOld,
};
},
queueMicrotask,
Expand Down

0 comments on commit 0d9b9ca

Please sign in to comment.