-
Notifications
You must be signed in to change notification settings - Fork 134
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
Module customization hooks and worker threads #1566
Comments
Let's revert the release branch to deal with the reported breakage there and get a new release out, even if that means continuing with the original bug that this change was meant to fix. In main we have a couple possible paths forward, I think @mcollina has an alternative approach in mind and we're just waiting for the PR to be opened. |
I think when unexpected breakage od this magnitude happens, it's best to revert an reapply later. This situation has brought up a few use cases that were covered by unit tests but not in the radar of the loaders team. It's better to regroup and reassess the situation. Telling people that were using threads before to use processes instead is 10 years backwards in the history of Node.js. I think we are past that point. If this is the recommendation, we should remove threads. In all intents and purposes I cannot endorse that as a technical priority for the project. |
Something that was expressed to me about the idea of hooks threads that correspond to loader pools is that such an architecture is very complicated both to build and to reason about, and would entail significant additional infrastructure: we’d need to provide APIs for spawning a defined number of hooks threads, for determining when to terminate the hooks threads (should they survive the pool they were meant to serve?) and so on. The intended design of a single hooks thread that all process threads share is much simpler. Consider the meeting where this was decided, back in 2022:
It’s more likely that a hooks author will want to share hooks state between all threads of the process, than to have separate hooks state per application thread. A transpilation hook will want to transpile a file once and keep the result in memory, not transpile it again and again for each thread. A testing framework might want to create a mock that applies to all threads. Instrumentation libraries would want to have only one copy of their library running, not one copy per thread. I feel pretty sure that a single hooks thread should be our default behavior, and something more sophisticated can come later (or not at all; I’m genuinely concerned about our ability to maintain even the level of complexity that we already have). I think we could add Ultimately this is about the conception of how people think about threads. Are they part of an existing app, spreading out CPU-intensive to multiple cores, or are they basically pseudo-child processes where you can launch mini-Nodes in different ways to do various different things? The desire to have different hooks for different threads relates to the latter concept, which is why child processes are a workable alternative. We only have this issue in the first place because of |
Let's separate the concerns here. Issue 1: Reverting the change in the release branch to unbreak the folks this change broke. This should be our immediate first action. Issue 2: What is the long term correct fix and what steps do we take to get there that (a) accomplishes the goal AND (b) minimizes breaks in the ecosystem. There is a path forward here where we are working with the ecosystem and not in spite of them. Before we tackle issue 2, let's deal with issue 1 which is the more pressing matter. |
The current list of broken things:
The latter is quite important, because it's built around the concept of using worker threads for isolation. The community is telling us it is a valid use case for threads. (Note that I'm 100% supportive of sharing one loader thread, I think this implementation landed too early). I consider not restoring this functionality as soon as possible is disrespectful for our users. How many wasted hours of dev time should we push to the module authors? |
Can you share the issues for esmock and ava? I’ve only seen the issue for Angular. |
There aren’t public issues, I’ve just tried them. |
So say we do step 1 here, of reverting on the release branch. Then presumably we land nodejs/node#53200 on We can’t restore the 22.1.0 behavior and migrate to a single hooks thread. So even after the revert in 22.3.0 changes us back to the 22.1.0 behavior, eventually we’re going to ship another intentional change that will force some people to react to. So now we cause changes twice: in 22.3.0, and again in some future version when we try again to ship the “one hooks thread” design, possibly with something in addition to opt into scoped hooks. Maybe that’s better than just shipping the bugfix? I dunno. All I know is that I’ve heard very consistent feedback that library authors want us to minimize the number of changes they need to react to. With regard to Ava, it already has a I don’t know what issue esmock could be having. It only added a reference to Lastly, it may be that I was wrong to assume that worker threads are faster than child processes; apparently it’s the reverse: https://github.com/orgs/nodejs/discussions/44264. So encouraging people to migrate from |
Please consider my two cents. Note: If no change in behaviour compared to the behaviour in 22.1.0 is acceptable, then the only short term way forward is the revert. Having said that, when I started working actively on this was after some time of following the discussions (on and off). From my perspective we needed a solution to reduce the number of threads from 2*N to N+1 in order to reduce resource consumption, etc. This kind of change is intrusive by definition and IMO it is expected that changes in behaviour will occur. We unfortunatelly introduced the hanging process bug with that PR (unintentedly, needles to say). #53200 tries to fix that. But one more decision of #52706 (obviously not an easy one) was that Supporting Personally, I would opt for a solution based on nodejs/node#53200 (comment). But #53200 needs some work after the last discussions and it would be good to be able to do that work without the release next week pressure. I also think that the API openness of both And then there's the concern of hooks polluting the state on the hooks thread in a way that impacts other threads. I think our open interface makes room for this kind of bugs (that isolation of worker threads tries to solve) |
I personally think this is fixable, and a design that can cater to most use cases can be done. Even if not, it should be clearly communicated ahead of time and circulated widely. What we need to do now is to restore a situation that did not break users, and then design something that can work for everybody. |
Let's land the revert now, and then let's open a "revert revert" PR that can also land immediately with |
@aduh95 When you talk about |
@ShogunPanda I think the double revert make the job easier for the releasers. |
Oh, I see. It makes totally sense. :) |
I'm okay with this if the latter PR can't get blocked. Otherwise we can just revert on the release branch. |
I have no problem iterating on the feature in the main or just working on a new implementation that does not have these issues on a fresh PR. I'm okay as long as we don't break constituents (or we break them consciously). |
I agree. How about this:
Step 2 fixes the cases that @ShogunPanda provided in his reproduction, and step 3 would fix the Angular case. Angular’s old code wouldn’t just start working again, but adapting to this change would be very minor: instead of importing This would fix the “more than one hooks threads getting spawned” bug while preserving the ability for hooks to behave differently based on the thread that modules are on. The other advantage of step 3 is that it’s small in scale; there’s a chance it could be done before the 22.3.0 release, which would minimize churn. |
About #3 I'm currently working on https://github.com/ShogunPanda/node/tree/multiple-chains (based off nodejs/node#53200 that will address the |
That's not going to work well with the current way we're cutting releases, let's just fast-track the revert and the revert-revert, no one is going to block it. |
As long as it can’t be blocked, that’s fine, I’m happy to do whatever method makes releases easier. |
So just to build on my suggested way forward in #1566 (comment), let’s do whatever reverts and revert reverts or whatever the proper procedure is right after the next TSC meeting; in the TSC meeting we can confirm that everyone at that meeting is on board with that plan, and we can coordinate who’s doing what and ensure that we’ll have the proper approvals and no blocks. Does that work @aduh95? Then I think the priorities need to be:
I think we have a good chance of landing the first two before 22.3.0, in which case we can release nodejs/node#52706, nodejs/node#53200 and the I’m not aware of anyone currently relying on the ability of |
What's the point of waiting for the next TSC meeting? The revert is going to miss the next release on Tuesday which is a disservice to our users; if we all agree it should be reverted on 22.x, let's start with that. |
If nodejs/node#53200 including In short: for 22.3.0 we unbreak everyone who we know was broken by 22.2.0. In future releases we handle additional desired use cases. |
This comment was marked as spam.
This comment was marked as spam.
I have a feeling that the timeline is rushed, and at least one PR would need to be fast-tracked. The safest bet is to revert, and take a bit of time to verify the solution works in all design and in all constraints. The proposed fix introduces the possibility for one thread to alter the behavior of a sibling thread by issuing a I think the source of the confusion is Node.js supports having user threads with different responsibilities. The current implementation of "one loader chain" breaks this assumption without a way for the user to control what is happening. The solution @GeoffreyBooth is proposing is to "use child processes" if isolation is required. I don't think this is a good path for Node.js. As an example, pino spawns a thread for distributing logs to destinations (elasticsearch and the like) off the main thread. This has advantages for the end users, as it guarantees better performance as well as the ability to deliver the logs inside the I don't is something we want to rush as there are important decisions to be made. |
I agree. While @dygabo is doing a wonderful job, our time constraint for |
I don't think this is quite accurate. If each hook has access to each thread's |
Sorry I misunderstood the proposal. This can work. |
Okay. I’ve approved the revert, since based on nodejs/node#53195 (comment) it seems like my suggestion of Both @mcollina would you be okay with allowing nodejs/node#53200 to land if it includes what it has now plus some way for hooks to branch based on thread, like export function resolve(url, context, next) {
if (context.workerData?.translations === 'italian') {
// Do something specific for the thread with the Italian translations or similar (like maybe it’s And then the “separate chains” idea can be a follow-up. |
I'm not convinced that it would solve all cases, and all the deduplication issues would be solved in time. I fear we are going to rush something out that would be differently broken vs the status quo, confusing our users further. |
fwiw the most difficult issue that I see now with the single thread approach is that it runs code initiated by different threads. Even if this is solvable at a logical level (offer the hook all data to decide if it acts or skips based on I'm not sure if there is an agreement on the lifespan of the registered hooks either. If a hook was registered by a call on |
The way I see it is that If a worker thread wants to register hooks that affect only that thread, it can do so by passing data such as that thread’s // worker.js
import { register } from 'node:module'
import { threadId } from 'node:worker_threads'
register(new URL('./hooks.js', import.meta.url), {
data: { threadId }
})
// hooks.js
let registeringThreadId
export function initialize({ threadId }) {
registeringThreadId = threadId
}
export function resolve(url, context, next) {
if (context.threadId === registeringThreadId) {
// Do something only for the thread that registered this hook
}
next(url, context)
} |
We are not in agreement. I think having a register called from a worker thread changing the behavior of its parent or another worker thread could lead to situations that are surprising snd and impossible to debug. |
For visibility, I share the same sentiment as @mcollina. This approach somewhat breaks the fundamental concept of worker thread isolation, as initialization code in one worker thread can now affect other sibling threads and the parent process. |
Hello! |
With regard to these issues and PRs discussed in yesterday’s TSC meeting:
It appears that we have a solution in nodejs/node#53200. I spoke to @dygabo today (the author of that PR) and he thinks that he’s fixed the bug and satisfied all the notes that people have left, other than @mcollina’s request (which I’ll get to in a minute).
In particular, what nodejs/node#53200 fixes is to make the prior PR, nodejs/node#52706, work as intended: calling
register
fromnew Worker
execArgv
no longer causes a hang, but registers hooks on the one hooks thread that nodejs/node#52706 created (replacing the prior architecture of separate hooks threads per worker thread). This has been the intended architecture since we first moved the hooks off-thread, and has been the plan since 2022. The issue I opened last year about the bug where there are multiple hooks threads, nodejs/node#50752, has 22 👍 s and this architecture was discussed in loaders meetings with stakeholders so i do think it’s the design favored by the majority of the community, though clearly there are some who prefer the alternative approach and consider moving to a single thread to be a regression.In particular, @mcollina’s objection to nodejs/node#53200 is that it doesn’t revert the change intended by nodejs/node#52706: he wants multiple hooks threads, though he didn’t object to nodejs/node#52706 or the previous threads where this was discussed.
What’s on
main
right now is what no one wants; either we should fix the single-hooks-thread implementation or revert it. On the one hand I sympathize with the users like Matteo who were affected by this change, as even the bugfixed version will still cause them to need to update their code to compensate for the new architecture; the Angular team has already done so in angular/angular-cli#27721, which fixes their app even for Node 22.2.0 with the hanging bug. That PR demonstrates a workaround for the new behavior, for people who really want scoped hooks: use child processes instead of worker threads. I assume this approach is probably slower, but it works. And it doesn’t need to be the end state; we could very well ship a new way to provide scoped hooks, and a discussion has begun in nodejs/node#53195 to brainstorm options.The option I don’t think we’re likely to choose is the prior state of how things were in 22.1.0, where every worker thread got its own hooks thread, regardless if that’s what the user wanted. Many of the options we’re discussing involve scoping the hooks in a more limited way, where they can behave differently per thread but still run on a single hooks thread rather than N hooks threads. So I’m hesitant to revert to the 22.1.0 behavior (which, to be clear, was buggy and unintentional) when I expect it will just change again. My preference would be to land nodejs/node#53200 to fix the current implementation, land at least some documentation explaining the child processes workaround, and continue to evaluate options for the scoped hooks feature request and potentially land something to satisfy those use cases when we have a solution that seems good to everyone. This would minimize churn, as the majority of users who aren’t trying to get scoped hooks won’t be affected by the future additions to support scoped hooks.
Alternatively we could revert back to 22.1.0 and then eventually land the change that moves to a single hooks thread alongside some new API that provides scoped hooks. This would involve more churn for users, and probably push out the overall API becoming stable for quite a while.
cc @nodejs/loaders @nodejs/tsc
The text was updated successfully, but these errors were encountered: