Skip to content

Conversation

@ShogunPanda
Copy link
Contributor

@ShogunPanda ShogunPanda commented Aug 31, 2025

This PR adds a new notification API to the worker module made of the following methods:

  • registerNotification
  • sendNotification
  • unregisterNotification
  • unregisterNotifications
  • getNotifications

The idea behind it is being able to trigger a callback in another module without having to go throughout all workers messaging and event handling.

The API purposely does not allow to attach any data to the notifications. The two threads must use SharedArrayBuffers to share data.

The usecase is threads that must transfer data back and forth the quickest way possible. For instance to simulate a socket via threads.

This is the typical usage:

import {
  registerNotification,
  sendNotification,
  unregisterNotification,
  isMainThread,
  workerData,
  Worker
} from 'node:worker_threads`

if (!isMainThread) {
  sendNotification(workerData.notification)
} else {
  // registerNotification returns a bigint, so it's easy to send between threads.
  const notification = registerNotification(() => {
    console.log('INVOKED')
    worker.terminate()
    unregisterNotification(notification)
  })

  new Worker(import.meta.filename, { workerData: { notification }));
}

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. labels Aug 31, 2025
@ShogunPanda ShogunPanda added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 31, 2025
@github-actions github-actions bot added request-ci-failed An error occurred while starting CI via request-ci label, and manual interventon is needed. and removed request-ci Add this label to start a Jenkins CI on a PR. labels Aug 31, 2025
@github-actions
Copy link
Contributor

Failed to start CI
   ⚠  No approving reviews found
   ✘  Refusing to run CI on potentially unsafe PR
https://github.com/nodejs/node/actions/runs/17355601584

@mcollina
Copy link
Member

Can you mark it as experimental?

@ShogunPanda
Copy link
Contributor Author

@mcollina Done

@mcollina
Copy link
Member

mcollina commented Sep 1, 2025

What's the performance gap between this and postMessage?

I know it might sound hard work, but is there any optimization potential in postMessage?

@codecov
Copy link

codecov bot commented Sep 1, 2025

Codecov Report

❌ Patch coverage is 92.74194% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.28%. Comparing base (34cb10b) to head (12ea147).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
src/env.cc 85.93% 3 Missing and 6 partials ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main   #59691    +/-   ##
========================================
  Coverage   88.27%   88.28%            
========================================
  Files         701      701            
  Lines      206640   206764   +124     
  Branches    39739    39748     +9     
========================================
+ Hits       182406   182535   +129     
+ Misses      16277    16269     -8     
- Partials     7957     7960     +3     
Files with missing lines Coverage Δ
lib/internal/bootstrap/node.js 99.58% <100.00%> (+<0.01%) ⬆️
lib/internal/process/per_thread.js 99.49% <100.00%> (-0.33%) ⬇️
src/env.h 98.14% <ø> (ø)
src/node_errors.h 87.50% <ø> (ø)
src/node_process_methods.cc 88.12% <100.00%> (+0.31%) ⬆️
src/env.cc 81.02% <85.93%> (+0.16%) ⬆️

... and 36 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Member

@addaleax addaleax left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API purposely does not allow to attach any data to the notifications. The two threads must use SharedArrayBuffers to share data.

Yeah, I'd say you're on the right track here – if you want the fastest possible low-level communication between threads, SharedArrayBuffers and Atomics is the best way to do this. To be completely honest, it seems like this API here would not add value on top of that, so the fact that it brings in additional complexity that would need to be maintained would make me suggest not to add it.

For this PR specifically, the current implementation breaks with a number of contracts and conventions for Node.js internals; if you want to pursue adding this API, I'd highly encourage looking at our src/README.md file for information about how to work with the C++ side of Node.js internals, and look at other places where we integrate with e.g. libuv handles and asynchronous messaging between threads.

@mcollina
Copy link
Member

mcollina commented Sep 5, 2025

Yeah, I'd say you're on the right track here – if you want the fastest possible low-level communication between threads, SharedArrayBuffers and Atomics is the best way to do this. To be completely honest, it seems like this API here would not add value on top of that, so the fact that it brings in additional complexity that would need to be maintained would make me suggest not to add it.

I've measured the approach with SharedArrayBuffers and Atomics, and unfortunately it's slower than our postMessage implementation.

@addaleax
Copy link
Member

addaleax commented Sep 5, 2025

I've measured the approach with SharedArrayBuffers and Atomics, and unfortunately it's slower than our postMessage implementation.

@mcollina That's very surprising, yes. Have we to talked to the V8 team about this? Atomics being slower than regular asynchronous communication seems like something that would be a mistake, honestly.

@ShogunPanda
Copy link
Contributor Author

@addaleax

Thanks a huge lot for your thorough review.
While trying to implement your requests I learned a lot of stuff and ultimately I was able to vastly improve and simplify the implementation.
Now everything is centered around Environments without additional classes. Also, I switched to a single uv_async per env relying on libuv coalescing rule.
Can you please review it again and tell me what to improve?

@jasnell
Thanks for all your inputs as well. A lot of edge case I totally forgot about.
To address your question, the reasoning behind this API is to provide the user a way to have a thread to say another thread "you can go on" without having to serialize or deserialize anything and without going across all the MessagePort machinery.

For the record, when I say "anything", I even include flipping SharedArrayBuffers bytes in order to use Atomics.wait and Atomics.notify.

Also, it will provide a node-ish natural way to synchronize work without blocking threads with Atomics.wait or Atomics.waitAsync.

@mcollina I'll try to provide benchmarks in the new few days.

@addaleax
Copy link
Member

addaleax commented Sep 8, 2025

Can you please review it again and tell me what to improve?

I think this is actually pretty good from an implementation perspective, aside from the lack of async tracking.

To address your question, the reasoning behind this API is to provide the user a way to have a thread to say another thread "you can go on" without having to serialize or deserialize anything and without going across all the MessagePort machinery.

Yeah, I think the problem is that you're mostly describing how this API differs from the others, but not why that would serve real-world use cases.

It seems like you're saying "this is better than MessagePort because MessagePort serializes data", but that leaves the bigger question of "could we achieve the same thing by not serializing and deserializing data if postMessage() is called without arguments to serialize?" unanswered – because if we can achieve the same thing that way, that seems preferable.

For the record, when I say "anything", I even include flipping SharedArrayBuffers bytes in order to use Atomics.wait and Atomics.notify.

Why is it bad to expect a user to change a byte in shared memory for communication purposes?

Also, it will provide a node-ish natural way to synchronize work without blocking threads with Atomics.wait or Atomics.waitAsync.

Atomics.waitAsync() doesn't block the thread though, that's specifically what it's there for.


So, to be clear, the big issues here are:

  • This expands API surface with a non-standard API, while there are standard APIs that we implement, that are available to users, and that provide the same functionality. Additional API surface means additional code and additional long-term maintenance burden.
  • If this is, in fact, more performant, then it's not clear whether this is inherent to this approach or it's something that another approach (like simplifying postMessage() in the no-argument case) could achieve as well.
  • There's a missing real-world usage scenario. Why would a thread wait for another thread to signal it to "go on" but not at the same time expect any data to be passed along with that signal? One-bit messaging between threads is a thing (e.g. signaling for condition variables), but that's (almost?) exclusively used in situations in which there is other data being passed along as well. Or, the other way around: Why would a thread wait in order to do something if it already has all relevant information available anyway?

v8::Local<v8::Function> callback =
notifications_callbacks_[id].Get(isolate);
MakeCallback(isolate, process, callback, 0, nullptr, {0, 0})
.ToLocalChecked();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid using ToLocalChecked if possible, if the callbacks throw it will crash the process. You'll want to make sure the error gets appropriately propagated.

Copy link
Contributor Author

@ShogunPanda ShogunPanda Sep 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed that. But I see no difference in how the error is propagated, I still get:

node:internal/event_target:1101
  process.nextTick(() => { throw err; });
                           ^
Error: kaboom
    at process.<anonymous> (/Volumes/DATI/Users/Shogun/Programmazione/OSS/nodejs/test/parallel/test-process-notifications-error.js:22:11)

Node.js v25.0.0-pre

Am I missing anything?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. request-ci-failed An error occurred while starting CI via request-ci label, and manual interventon is needed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants