Skip to content

Commit

Permalink
process: add deferTick
Browse files Browse the repository at this point in the history
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.

TODO:
- [] Fill in concrete examples
- [] Add tests
- [] Add benchmarks
- [] Do we need async hook logic or not?
- [] process._exiting?
- [] Anything else we are unhappy with in regard to nextTick which
     we want to fix?
  • Loading branch information
ronag committed Jan 15, 2024
1 parent 94f824a commit e3ffd48
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 1 deletion.
19 changes: 19 additions & 0 deletions doc/api/process.md
Original file line number Diff line number Diff line change
Expand Up @@ -1219,6 +1219,25 @@ 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 zaldo when
combinding traditional node asynchronous code with Promises.

## `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
31 changes: 31 additions & 0 deletions lib/internal/process/task_queues.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ function setHasTickScheduled(value) {
}

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

// Should be in sync with RunNextTicksNative in node_task_queue.cc
function runNextTicks() {
Expand Down Expand Up @@ -93,6 +94,10 @@ function processTicksAndRejections() {
emitAfter(asyncId);
}
runMicrotasks();

let tmp = queue;

Check failure on line 98 in lib/internal/process/task_queues.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

'tmp' is never reassigned. Use 'const' instead
queue = deferQueue;

Check failure on line 99 in lib/internal/process/task_queues.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

'queue' is constant
deferQueue = tmp;

Check failure on line 100 in lib/internal/process/task_queues.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

'deferQueue' is constant
} while (!queue.isEmpty() || processPromiseRejections());
setHasTickScheduled(false);
setHasRejectionToWarn(false);
Expand Down Expand Up @@ -133,6 +138,31 @@ function nextTick(callback) {
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,
};

if (initHooksExist())
emitInit(asyncId, 'TickObject', triggerAsyncId, tickObject);

deferQueue.push(tickObject);
}

function runMicrotask() {
this.runInAsyncScope(() => {
const callback = this.callback;
Expand Down Expand Up @@ -166,6 +196,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(() => {}));

Check failure on line 14 in test/async-hooks/test-defertick.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Do not use an empty function, omit the parameter altogether
});

0 comments on commit e3ffd48

Please sign in to comment.