Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 8979863

Browse files
committed
process: add deferTick
Adds a new scheduling primitive to resolve zaldo when mixing traditional Node async programming with async/await and Promises. We cannot "fix" nextTick without breaking the whole ecosystem. nextTick usage should be discouraged and we should try to incrementally move to this new primitive. Refs: nodejs#51156 Refs: nodejs#51280 Refs: nodejs#51114 Refs: nodejs#51070 Refs: nodejs/undici#2497 PR-URL: nodejs#51471
1 parent 94f824a commit 8979863

File tree

4 files changed

+123
-27
lines changed

4 files changed

+123
-27
lines changed

doc/api/process.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1219,6 +1219,75 @@ const process = require('node:process');
12191219
process.debugPort = 5858;
12201220
```
12211221

1222+
## `process.deferTick(callback[, ...args])`
1223+
1224+
<!-- YAML
1225+
added: REPLACEME
1226+
-->
1227+
1228+
* `callback` {Function}
1229+
* `...args` {any} Additional arguments to pass when invoking the `callback`
1230+
1231+
`process.deferTick()` adds `callback` to the "defer tick queue". This queue is
1232+
fully drained after the current operation on the JavaScript stack runs to
1233+
completion and before the event loop is allowed to continue. It's possible to
1234+
create an infinite loop if one were to recursively call `process.deferTick()`.
1235+
See the [Event Loop][] guide for more background.
1236+
1237+
Unlike `process.nextTick`, `process.deferTick()` will run after the "next tick
1238+
queue" and the microtask queue has been fully drained as to avoid Zalgo when
1239+
combinding traditional node asynchronous code with Promises.
1240+
1241+
Consider the following example:
1242+
1243+
```js
1244+
// uncaughtException
1245+
setImmediate(async () => {
1246+
const e = await new Promise(resolve => {
1247+
const e = new EventEmitter()
1248+
resolve(e)
1249+
process.nextTick(() => {
1250+
e.emit('error', new Error('setImmediate'))
1251+
})
1252+
})
1253+
e.on('error', () => {})
1254+
})
1255+
1256+
// uncaughtException
1257+
setImmediate(async () => {
1258+
const e = await new Promise(resolve => {
1259+
const e = new EventEmitter()
1260+
resolve(e)
1261+
queueMicrotask(() => {
1262+
e.emit('error', new Error('setImmediate'))
1263+
})
1264+
})
1265+
e.on('error', () => {})
1266+
})
1267+
```
1268+
1269+
In both of these cases the user will encounter an
1270+
`uncaughtException´ error since the inner task
1271+
will execute before control is returned to the
1272+
caller of `await`. In order to fix this one should
1273+
use `process.deferTick` which will execute in the
1274+
expected order:
1275+
1276+
```js
1277+
// OK!
1278+
setImmediate(async () => {
1279+
const e = await new Promise(resolve => {
1280+
const e = new EventEmitter()
1281+
resolve(e)
1282+
process.deferTick(() => {
1283+
e.emit('error', new Error('setImmediate'))
1284+
})
1285+
})
1286+
e.on('error', () => {})
1287+
})
1288+
```
1289+
1290+
12221291
## `process.disconnect()`
12231292

12241293
<!-- YAML

lib/internal/bootstrap/node.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,8 +303,9 @@ process.emitWarning = emitWarning;
303303
// bootstrap to make sure that any operation done before this are synchronous.
304304
// If any ticks or timers are scheduled before this they are unlikely to work.
305305
{
306-
const { nextTick, runNextTicks } = setupTaskQueue();
306+
const { nextTick, runNextTicks, deferTick } = setupTaskQueue();
307307
process.nextTick = nextTick;
308+
process.deferTick = deferTick;
308309
// Used to emulate a tick manually in the JS land.
309310
// A better name for this function would be `runNextTicks` but
310311
// it has been exposed to the process object so we keep this legacy name

lib/internal/process/task_queues.js

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -44,21 +44,14 @@ const { AsyncResource } = require('async_hooks');
4444
// *Must* match Environment::TickInfo::Fields in src/env.h.
4545
const kHasTickScheduled = 0;
4646

47-
function hasTickScheduled() {
48-
return tickInfo[kHasTickScheduled] === 1;
49-
}
50-
51-
function setHasTickScheduled(value) {
52-
tickInfo[kHasTickScheduled] = value ? 1 : 0;
53-
}
54-
55-
const queue = new FixedQueue();
47+
let queue = new FixedQueue();
48+
let deferQueue = new FixedQueue();
5649

5750
// Should be in sync with RunNextTicksNative in node_task_queue.cc
5851
function runNextTicks() {
59-
if (!hasTickScheduled() && !hasRejectionToWarn())
52+
if (tickInfo[kHasTickScheduled] === 0 && !hasRejectionToWarn())
6053
runMicrotasks();
61-
if (!hasTickScheduled() && !hasRejectionToWarn())
54+
if (tickInfo[kHasTickScheduled] === 0 && !hasRejectionToWarn())
6255
return;
6356

6457
processTicksAndRejections();
@@ -93,46 +86,63 @@ function processTicksAndRejections() {
9386
emitAfter(asyncId);
9487
}
9588
runMicrotasks();
89+
90+
const tmp = queue;
91+
queue = deferQueue;
92+
deferQueue = tmp;
9693
} while (!queue.isEmpty() || processPromiseRejections());
97-
setHasTickScheduled(false);
94+
tickInfo[kHasTickScheduled] = 0;
9895
setHasRejectionToWarn(false);
9996
}
10097

10198
// `nextTick()` will not enqueue any callback when the process is about to
10299
// exit since the callback would not have a chance to be executed.
103-
function nextTick(callback) {
100+
function nextTick(callback, ...args) {
104101
validateFunction(callback, 'callback');
105102

106103
if (process._exiting)
107104
return;
108105

109-
let args;
110-
switch (arguments.length) {
111-
case 1: break;
112-
case 2: args = [arguments[1]]; break;
113-
case 3: args = [arguments[1], arguments[2]]; break;
114-
case 4: args = [arguments[1], arguments[2], arguments[3]]; break;
115-
default:
116-
args = new Array(arguments.length - 1);
117-
for (let i = 1; i < arguments.length; i++)
118-
args[i - 1] = arguments[i];
106+
if (tickInfo[kHasTickScheduled] === 0) {
107+
tickInfo[kHasTickScheduled] = 1;
119108
}
120109

121-
if (queue.isEmpty())
122-
setHasTickScheduled(true);
123110
const asyncId = newAsyncId();
124111
const triggerAsyncId = getDefaultTriggerAsyncId();
125112
const tickObject = {
126113
[async_id_symbol]: asyncId,
127114
[trigger_async_id_symbol]: triggerAsyncId,
128115
callback,
129-
args,
116+
args: args.length > 0 ? args : null,
130117
};
131118
if (initHooksExist())
132119
emitInit(asyncId, 'TickObject', triggerAsyncId, tickObject);
133120
queue.push(tickObject);
134121
}
135122

123+
function deferTick(callback, ...args) {
124+
validateFunction(callback, 'callback');
125+
126+
if (process._exiting)
127+
return;
128+
129+
if (tickInfo[kHasTickScheduled] === 0) {
130+
tickInfo[kHasTickScheduled] = 1;
131+
}
132+
133+
const asyncId = newAsyncId();
134+
const triggerAsyncId = getDefaultTriggerAsyncId();
135+
const tickObject = {
136+
[async_id_symbol]: asyncId,
137+
[trigger_async_id_symbol]: triggerAsyncId,
138+
callback,
139+
args: args.length > 0 ? args : null,
140+
};
141+
if (initHooksExist())
142+
emitInit(asyncId, 'TickObject', triggerAsyncId, tickObject);
143+
deferQueue.push(tickObject);
144+
}
145+
136146
function runMicrotask() {
137147
this.runInAsyncScope(() => {
138148
const callback = this.callback;
@@ -166,6 +176,7 @@ module.exports = {
166176
setTickCallback(processTicksAndRejections);
167177
return {
168178
nextTick,
179+
deferTick,
169180
runNextTicks,
170181
};
171182
},

test/async-hooks/test-defertick.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const { EventEmitter } = require('events');
5+
6+
setImmediate(async () => {
7+
const e = await new Promise((resolve) => {
8+
const e = new EventEmitter();
9+
resolve(e);
10+
process.deferTick(common.mustCall(() => {
11+
e.emit('error', new Error('kaboom'));
12+
}));
13+
});
14+
e.on('error', common.mustCall(() => {}));
15+
});

0 commit comments

Comments
 (0)