fix(core): ensure create nodes functions are properly parallelized (#23005)

This commit is contained in:
Craigory Coppola 2024-04-25 16:45:26 -04:00 committed by GitHub
parent c6fe9696fd
commit 0fd6d23e3f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 159 additions and 37 deletions

View File

@ -6,7 +6,7 @@ Context for [CreateNodesFunction](../../devkit/documents/CreateNodesFunction)
### Properties ### Properties
- [configFiles](../../devkit/documents/CreateNodesContext#configfiles): string[] - [configFiles](../../devkit/documents/CreateNodesContext#configfiles): readonly string[]
- [nxJsonConfiguration](../../devkit/documents/CreateNodesContext#nxjsonconfiguration): NxJsonConfiguration<string[] | "\*"> - [nxJsonConfiguration](../../devkit/documents/CreateNodesContext#nxjsonconfiguration): NxJsonConfiguration<string[] | "\*">
- [workspaceRoot](../../devkit/documents/CreateNodesContext#workspaceroot): string - [workspaceRoot](../../devkit/documents/CreateNodesContext#workspaceroot): string
@ -14,7 +14,7 @@ Context for [CreateNodesFunction](../../devkit/documents/CreateNodesFunction)
### configFiles ### configFiles
`Readonly` **configFiles**: `string`[] `Readonly` **configFiles**: readonly `string`[]
The subset of configuration files which match the createNodes pattern The subset of configuration files which match the createNodes pattern

View File

@ -22,7 +22,7 @@ export interface CreateNodesContext {
/** /**
* The subset of configuration files which match the createNodes pattern * The subset of configuration files which match the createNodes pattern
*/ */
readonly configFiles: string[]; readonly configFiles: readonly string[];
} }
/** /**

View File

@ -0,0 +1,123 @@
import { runCreateNodesInParallel } from './utils';
const configFiles = ['file1', 'file2'] as const;
const context = {
file: 'file1',
nxJsonConfiguration: {},
workspaceRoot: '',
configFiles,
} as const;
describe('createNodesInParallel', () => {
it('should return results with context', async () => {
const plugin = {
name: 'test',
createNodes: [
'*/**/*',
async (file: string) => {
return {
projects: {
[file]: {
root: file,
},
},
};
},
],
} as const;
const options = {};
const results = await runCreateNodesInParallel(
configFiles,
plugin,
options,
context
);
expect(results).toMatchInlineSnapshot(`
[
{
"file": "file1",
"pluginName": "test",
"projects": {
"file1": {
"root": "file1",
},
},
},
{
"file": "file2",
"pluginName": "test",
"projects": {
"file2": {
"root": "file2",
},
},
},
]
`);
});
it('should handle async errors', async () => {
const plugin = {
name: 'test',
createNodes: [
'*/**/*',
async () => {
throw new Error('Async Error');
},
],
} as const;
const options = {};
const error = await runCreateNodesInParallel(
configFiles,
plugin,
options,
context
).catch((e) => e);
expect(error).toMatchInlineSnapshot(
`[AggregateCreateNodesError: Failed to create nodes]`
);
expect(error.errors).toMatchInlineSnapshot(`
[
[CreateNodesError: The "test" plugin threw an error while creating nodes from file1:],
[CreateNodesError: The "test" plugin threw an error while creating nodes from file2:],
]
`);
});
it('should handle sync errors', async () => {
const plugin = {
name: 'test',
createNodes: [
'*/**/*',
() => {
throw new Error('Sync Error');
},
],
} as const;
const options = {};
const error = await runCreateNodesInParallel(
configFiles,
plugin,
options,
context
).catch((e) => e);
expect(error).toMatchInlineSnapshot(
`[AggregateCreateNodesError: Failed to create nodes]`
);
expect(error.errors).toMatchInlineSnapshot(`
[
[CreateNodesError: The "test" plugin threw an error while creating nodes from file1:],
[CreateNodesError: The "test" plugin threw an error while creating nodes from file2:],
]
`);
});
});

View File

@ -9,7 +9,12 @@ import type {
LoadedNxPlugin, LoadedNxPlugin,
NormalizedPlugin, NormalizedPlugin,
} from './internal-api'; } from './internal-api';
import type { CreateNodesContext, NxPlugin, NxPluginV2 } from './public-api'; import {
CreateNodesResult,
type CreateNodesContext,
type NxPlugin,
type NxPluginV2,
} from './public-api';
import { AggregateCreateNodesError, CreateNodesError } from '../error-types'; import { AggregateCreateNodesError, CreateNodesError } from '../error-types';
export function isNxPluginV2(plugin: NxPlugin): plugin is NxPluginV2 { export function isNxPluginV2(plugin: NxPlugin): plugin is NxPluginV2 {
@ -49,7 +54,7 @@ export function normalizeNxPlugin(plugin: NxPlugin): NormalizedPlugin {
} }
export async function runCreateNodesInParallel( export async function runCreateNodesInParallel(
configFiles: string[], configFiles: readonly string[],
plugin: NormalizedPlugin, plugin: NormalizedPlugin,
options: unknown, options: unknown,
context: CreateNodesContext context: CreateNodesContext
@ -59,39 +64,33 @@ export async function runCreateNodesInParallel(
const errors: CreateNodesError[] = []; const errors: CreateNodesError[] = [];
const results: CreateNodesResultWithContext[] = []; const results: CreateNodesResultWithContext[] = [];
const promises: Array<Promise<void>> = configFiles.map((file) => { const promises: Array<Promise<void>> = configFiles.map(async (file) => {
performance.mark(`${plugin.name}:createNodes:${file} - start`); performance.mark(`${plugin.name}:createNodes:${file} - start`);
// Result is either static or a promise, using Promise.resolve lets us try {
// handle both cases with same logic const value = await plugin.createNodes[1](file, options, context);
const value = Promise.resolve( if (value) {
plugin.createNodes[1](file, options, context) results.push({
); ...value,
return value file,
.catch((e) => { pluginName: plugin.name,
performance.mark(`${plugin.name}:createNodes:${file} - end`); });
errors.push( }
new CreateNodesError({ } catch (e) {
error: e, errors.push(
pluginName: plugin.name, new CreateNodesError({
file, error: e,
}) pluginName: plugin.name,
); file,
return null; })
}) );
.then((r) => { } finally {
performance.mark(`${plugin.name}:createNodes:${file} - end`); performance.mark(`${plugin.name}:createNodes:${file} - end`);
performance.measure( performance.measure(
`${plugin.name}:createNodes:${file}`, `${plugin.name}:createNodes:${file}`,
`${plugin.name}:createNodes:${file} - start`, `${plugin.name}:createNodes:${file} - start`,
`${plugin.name}:createNodes:${file} - end` `${plugin.name}:createNodes:${file} - end`
); );
}
// Existing behavior is to ignore null results of
// createNodes function.
if (r) {
results.push({ ...r, file, pluginName: plugin.name });
}
});
}); });
await Promise.all(promises).then(() => { await Promise.all(promises).then(() => {