Skip to content
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

Execute iterator exploration #1

Closed
wants to merge 10 commits into from
Closed

Conversation

dpchamps
Copy link
Owner

@dpchamps dpchamps commented Sep 23, 2021

Current Behavior

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)

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!


return args;
}
const nextIterValue = (typedIter: TypedIterator) =>
Copy link
Owner Author

@dpchamps dpchamps Sep 23, 2021

Choose a reason for hiding this comment

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

Typing this is quite challenging. We need to indicate that the result of the iterator is either one type or the other so that we can map the actual value and decorate the result with some data needed to "schedule" them.

]);

while (iteratorMap.size > 0) {
const result = await Promise.race(iteratorMap.values());
Copy link
Owner Author

@dpchamps dpchamps Sep 23, 2021

Choose a reason for hiding this comment

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

Greedily take the next ready value.

Consume each individual iterator lazily: Only tick the last iterator to have won the race.

runProcess(event, options);
}
if (result.type === 'build' && result.value.success) {
await subProcessRunner.handleBuildEvent(result.value);
Copy link
Owner Author

Choose a reason for hiding this comment

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

Guarantee that a build event resets the process runner with the latest build.

return deferred;
};

export class PromiseQueue<T> {
Copy link
Owner Author

Choose a reason for hiding this comment

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

This data structure is necessary to take the push-based child process into a pull-based async iterator

@@ -97,3 +98,15 @@ export interface NormalizedBuildNodeBuilderOptions
extends BuildNodeBuilderOptions {
webpackConfig: string[];
}

export interface NodeExecuteBuilderOptions {
Copy link
Owner Author

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.

Comment on lines +223 to +226
(devkit.runExecutor as any).mockImplementation(function* () {
yield { success: true, outfile: 'outfile.js' };
yield { success: true, outfile: 'outfile.js' };
});
Copy link
Owner Author

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.

@@ -83,6 +88,7 @@ describe('NodeExecuteBuilder', () => {

it('should build the application and start the built file', async () => {
for await (const event of executeExecutor(testOptions, context)) {
if (!('success' in event)) throw new Error('Got Unexpected event');
Copy link
Owner Author

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/dpchamps/nx/pull/1/files#diff-8b1daad050dbc745646b89f91cd20c497a23f51caf7eeaddb96a517a80b8f01fR28

error: unknown;
};

export class SubProcessRunner {
Copy link
Owner Author

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!

@dpchamps dpchamps closed this Sep 23, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
1 participant