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
fix(node): Cooperative Scheduling between startBuild and subProcess generators #7110
Conversation
This pull request is being automatically deployed with Vercel (learn more). 🔍 Inspect: https://vercel.com/nrwl/nx-dev/HPDNg9xaTVrx9CvSXgYsrYe5C7ei [Deployment for 26a1a0e canceled] |
return { | ||
on: (eventName, cb) => { | ||
mockSubProcess = { | ||
on: jest.fn().mockImplementation((eventName, cb) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Represents a breaking change: the type of event yielded by executeExecutor is now: https://github.com/nrwl/nx/pull/7110/files#diff-8b1daad050dbc745646b89f91cd20c497a23f51caf7eeaddb96a517a80b8f01fR28
@@ -215,6 +220,10 @@ describe('NodeExecuteBuilder', () => { | |||
}); | |||
|
|||
it('should log errors from killing the process', async () => { | |||
(devkit.runExecutor as any).mockImplementation(function* () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The old test cases for this spec and the one below were non-deterministic:
The state of subProcess was managed as a file-level global previously. These tests would only pass when at least one previous test had been run to populate the subProcess var.
With that subProcess state encapsulated, there was no way to make this test pass without altering it.
]); | ||
|
||
while (iteratorMap.size > 0) { | ||
const result = await Promise.race(iteratorMap.values()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the crux of the sln, greedily take the next ready value.
Consume each individual iterator lazily: Only tick the last iterator to have won the race. On Build event, reset the state of the subProcess iterator
@@ -97,3 +98,15 @@ export interface NormalizedBuildNodeBuilderOptions | |||
extends BuildNodeBuilderOptions { | |||
webpackConfig: string[]; | |||
} | |||
|
|||
export interface NodeExecuteBuilderOptions { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved this type into utils, because SubProcessRunner uses it as well.
error: unknown; | ||
}; | ||
|
||
export class SubProcessRunner { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Happy to write tests for this, awaiting feedback for where this should be in the project structure. It seems like it belongs colocated to executor.impl.ts, but I didn't see other examples of doing stuff like that.
The executor.impl tests cover the integration of this in full, but it felt big enough to split into a separate file. Let me know what you think!
const result = await Promise.race([ | ||
executorIterator.next(), | ||
new Promise(() => { | ||
mockSubProcessExit(1); | ||
events.push(firstEvent); | ||
}), | ||
]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const result = await Promise.race([ | |
executorIterator.next(), | |
new Promise(() => { | |
mockSubProcessExit(1); | |
events.push(firstEvent); | |
}), | |
]); | |
const result = await Promise.race([ | |
executorIterator.next(), | |
new Promise(() => { | |
mockSubProcessExit(1); | |
events.push(firstEvent); | |
// NOTE: this promise intentionally never resolves so that... | |
}), | |
]); |
This threw me off for a sec :) Might help to leave some breadcrumbs
Nx Cloud ReportCI is running for commit 26a1a0e. 📂 Click to track the progress, see the status, the terminal output, and the build insights. Sent with 💌 from NxCloud. |
3e6afa8
to
26a1a0e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems way too complicated to fix the issue outlined in #7031
I believe the fix should be that executeExecutor
should not yield until the subprocess dies?
- The
executeExecutor
keeps iterating over the build events - When it gets a build event, it starts the process
- Currently, it yields.. I don't think it should do this yet?
- It should wait until the subprocess exits to yield..
In general, we should be yielding results of executing the process here 👀
cc @AgentEnder could you take a look at this please?
@FrozenPandaz I agree it's a non trivial fix: From the issue:
After speaking with @llwt, we need ideally two things:
Consider your points 3 and 4:
How do we then both wait for the subprocess to complete (and also yield events from that subprocess) but also allow for new build events to propagate? For example, when I'm in More context, if neededThis is all happening within a `for await ... of` loop. Where we need to run one iterator to completion, but wait for at least the last side-effect to complete before returning. It's further complicated by the requirement of yielding events from that side-effect. So if subprocess events should be yielded here, then (as far as I can see) there's no way to do it without merging the two iterators to allow them to both "tick". SubProcess being push-based doesn't help the situation. I rolled the implementations myself to take an event emitter -> async iterator, but I'm sure there are third party libs to do that as well. Make sense? If there's a more obvious solution I'm happy to talk it out :) |
Bump @vsavkin / @AgentEnder, Anything we can do to move this along? Currently serving an app outside of watch mode is not possible and we're in a broken state since updating to the latest version of nx. Happy to modify the PR, but we need a bit of guidance as to what ya'll would like to see. We don't really see a simpler way to do this with the change to async iterators, however, maybe your eyes see something we don't? |
I'll take a look at this over the week, sorry for the delay. |
Thanks! |
Hey all, I just want to chime in with a respectful, kind plea for attention here. We've been blocked on this for a while now, and are eager to pay back the techdebt accrued while blocked by a fix. @dpchamps explanation is above is fine and good, as is @llwt 's in #7710. i want to emphasize how fundamental of a problem this is, perhaps to elevate the priority here. here's what we cant do right now: // my-executor.ts
runExecutor(nodeExecutorInput1)
runExecutor(nodeExecutorInput2) can't do it. this is because function runProcess(event, options) {
if (subProcess || !event.success) {
return rxjs_1.EMPTY;
}
return new rxjs_1.Observable((subscriber) => {
subProcess = child_process_1.fork(event.outfile, options.args, { // 👀 👀 👀
execArgv: getExecArgv(options),
}); both executions race to squash the We are willing to pair and discuss this further! :) I thought i'd try my hand in articulating the problem from a different perspective. Thanks |
@dpchamps Ok, I know I've said I'd look over this before but I'm going to spend some real time w/ this problem this week. At minimum this PR would need to be rebased, but lets chat some on the issue first to make sure this is still the solution that we should pursue. |
@cdaringe it seems like this issue might be a bit different to the above discussion, albeit it would disappear if you were able to wait for the first executor to finish... From the code sample it sounds more like you want to be able to run two node execute runs in parallel from a custom executor. It may be a good idea to open a new issue for that. |
@AgentEnder I met with @vsavkin a while back and he agreed to take these changes into a new more general executor. The "complicatedness" comes mostly from poor naming: its not "cooperative scheduling" so much as it is a need for some type merge+prioritization for two generators. After the discussion with Victor, we arrived at the general conclusion that these changes did not appear to be too complicated for the problem at hand. The issue that @cdaringe is calling out is fixed in this PR due to some non-determinism caused by having module-level mutable state. |
3a99403
to
334c230
Compare
@jaysoo I'm curious if you and team are planning on addressing @llwt's request in #7031:
This PR presented an idea for how to make that possible. We very much need this functionality. It's arguably more important to us than the initial bug presented, and is what @vsavkin and myself spent most of our time discussing in the call. |
This pull request has already been merged/closed. If you experience issues related to these changes, please open a new issue referencing this pull request. |
Current Behavior
This pr patches the bug outlined here: #7031
The node:execute builder immediately exits after build when --watch=false.
Expected Behavior
The node:execute builder wait for the subprocess to exit, or SIGTERM to be received, before exiting. This is how things worked previously.
Related Issue(s)
Fixes #7031
Notes
The solution here is to essentially "cooperatively schedule" (using the term loosely) events emitted from the
startBuild
generator, and the events emitted from the child process.We must allow build events to propagate (for example, when
watch
mode is configured), while also ensuring that the executor waits for the current process to run to completion.I've implemented some things as classes here, because my sense was this was more in the style of the repo. Happy to capture them differently, if the team would like. Have dropped some github comments along the way, happy to discuss the impl furthur!