Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

process: add deferTick #51471

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions doc/api/process.md
Original file line number Diff line number Diff line change
Expand Up @@ -1219,6 +1219,74 @@ const process = require('node:process');
process.debugPort = 5858;
```

## `process.deferTick(callback[, ...args])`

<!-- YAML
added: REPLACEME
-->

* `callback` {Function}
* `...args` {any} Additional arguments to pass when invoking the `callback`

`process.deferTick()` adds `callback` to the "defer tick queue". This queue is
fully drained after the current operation on the JavaScript stack runs to
completion and before the event loop is allowed to continue. It's possible to
create an infinite loop if one were to recursively call `process.deferTick()`.
See the [Event Loop][] guide for more background.

Unlike `process.nextTick`, `process.deferTick()` will run after the "next tick
queue" and the microtask queue has been fully drained as to avoid Zalgo when
combinding traditional node asynchronous code with Promises.
ronag marked this conversation as resolved.
Show resolved Hide resolved

Consider the following example:

```js
// uncaughtException!
setImmediate(async () => {
const e = await new Promise((resolve) => {
const e = new EventEmitter();
resolve(e);
process.nextTick(() => {
e.emit('error', new Error('process.nextTick'));
});
});
e.on('error', () => {}); // e.emit executes before we reach this...
});

// uncaughtException!
setImmediate(async () => {
const e = await new Promise((resolve) => {
const e = new EventEmitter();
resolve(e);
queueMicrotask(() => {
e.emit('error', new Error('queueMicrotask'));
});
});
e.on('error', () => {}); // e.emit executes before we reach this...
});
```

In both of these cases the user will encounter an
`uncaughtException` error since the inner task
will execute before control is returned to the
caller of `await`. In order to fix this one should
use `process.deferTick` which will execute in the
expected order:

```js
// OK!
setImmediate(async () => {
const e = await new Promise((resolve) => {
const e = new EventEmitter();
resolve(e);
process.deferTick(() => {
e.emit('error', new Error('process.deferTick'));
});
});
e.on('error', () => {}); // e.emit executes *after* we reach this.
});
```

## `process.disconnect()`

<!-- YAML
Expand Down
3 changes: 2 additions & 1 deletion lib/internal/bootstrap/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,9 @@ process.emitWarning = emitWarning;
// bootstrap to make sure that any operation done before this are synchronous.
// If any ticks or timers are scheduled before this they are unlikely to work.
{
const { nextTick, runNextTicks } = setupTaskQueue();
const { nextTick, runNextTicks, deferTick } = setupTaskQueue();
process.nextTick = nextTick;
process.deferTick = deferTick;
// Used to emulate a tick manually in the JS land.
// A better name for this function would be `runNextTicks` but
// it has been exposed to the process object so we keep this legacy name
Expand Down
76 changes: 40 additions & 36 deletions lib/internal/process/task_queues.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use strict';

const {
Array,
FunctionPrototypeBind,
} = primordials;

Expand Down Expand Up @@ -44,21 +43,14 @@ const { AsyncResource } = require('async_hooks');
// *Must* match Environment::TickInfo::Fields in src/env.h.
const kHasTickScheduled = 0;

function hasTickScheduled() {
return tickInfo[kHasTickScheduled] === 1;
}

function setHasTickScheduled(value) {
tickInfo[kHasTickScheduled] = value ? 1 : 0;
}

const queue = new FixedQueue();
let queue = new FixedQueue();
let deferQueue = new FixedQueue();

// Should be in sync with RunNextTicksNative in node_task_queue.cc
function runNextTicks() {
if (!hasTickScheduled() && !hasRejectionToWarn())
if (tickInfo[kHasTickScheduled] === 0 && !hasRejectionToWarn())
runMicrotasks();
if (!hasTickScheduled() && !hasRejectionToWarn())
if (tickInfo[kHasTickScheduled] === 0 && !hasRejectionToWarn())
return;

processTicksAndRejections();
Expand All @@ -72,18 +64,12 @@ function processTicksAndRejections() {
emitBefore(asyncId, tock[trigger_async_id_symbol], tock);

try {
const args = tock.args;
const callback = tock.callback;
if (tock.args === undefined) {
if (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);
}
callback(...args);
}
} finally {
if (destroyHooksExist())
Expand All @@ -93,46 +79,63 @@ function processTicksAndRejections() {
emitAfter(asyncId);
}
runMicrotasks();

const tmp = queue;
queue = deferQueue;
deferQueue = tmp;
} while (!queue.isEmpty() || processPromiseRejections());
setHasTickScheduled(false);
tickInfo[kHasTickScheduled] = 0;
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) {
function nextTick(callback, ...args) {
validateFunction(callback, 'callback');

if (process._exiting)
return;

let args;
switch (arguments.length) {
case 1: break;
case 2: args = [arguments[1]]; break;
case 3: args = [arguments[1], arguments[2]]; break;
case 4: args = [arguments[1], arguments[2], arguments[3]]; break;
default:
args = new Array(arguments.length - 1);
for (let i = 1; i < arguments.length; i++)
args[i - 1] = arguments[i];
if (tickInfo[kHasTickScheduled] === 0) {
tickInfo[kHasTickScheduled] = 1;
}

if (queue.isEmpty())
setHasTickScheduled(true);
const asyncId = newAsyncId();
const triggerAsyncId = getDefaultTriggerAsyncId();
const tickObject = {
[async_id_symbol]: asyncId,
[trigger_async_id_symbol]: triggerAsyncId,
callback,
args,
args: args.length > 0 ? args : undefined,
};
if (initHooksExist())
emitInit(asyncId, 'TickObject', triggerAsyncId, tickObject);
queue.push(tickObject);
}

function deferTick(callback, ...args) {
validateFunction(callback, 'callback');

if (process._exiting)
return;

if (tickInfo[kHasTickScheduled] === 0) {
tickInfo[kHasTickScheduled] = 1;
}

const asyncId = newAsyncId();
const triggerAsyncId = getDefaultTriggerAsyncId();
const tickObject = {
[async_id_symbol]: asyncId,
[trigger_async_id_symbol]: triggerAsyncId,
callback,
args: args.length > 0 ? args : undefined,
};
if (initHooksExist())
emitInit(asyncId, 'TickObject', triggerAsyncId, tickObject);
deferQueue.push(tickObject);
}

function runMicrotask() {
this.runInAsyncScope(() => {
const callback = this.callback;
Expand Down Expand Up @@ -166,6 +169,7 @@ module.exports = {
setTickCallback(processTicksAndRejections);
return {
nextTick,
deferTick,
runNextTicks,
};
},
Expand Down
15 changes: 15 additions & 0 deletions test/async-hooks/test-defertick.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use strict';

const common = require('../common');
const { EventEmitter } = require('events');

setImmediate(async () => {
const e = await new Promise((resolve) => {
const e = new EventEmitter();
resolve(e);
process.deferTick(common.mustCall(() => {
e.emit('error', new Error('kaboom'));
}));
});
e.on('error', common.mustCall());
});
Loading