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
- [configFiles](../../devkit/documents/CreateNodesContext#configfiles): string[]
- [configFiles](../../devkit/documents/CreateNodesContext#configfiles): readonly string[]
- [nxJsonConfiguration](../../devkit/documents/CreateNodesContext#nxjsonconfiguration): NxJsonConfiguration<string[] | "\*">
- [workspaceRoot](../../devkit/documents/CreateNodesContext#workspaceroot): string
@ -14,7 +14,7 @@ Context for [CreateNodesFunction](../../devkit/documents/CreateNodesFunction)
### configFiles
`Readonly` **configFiles**: `string`[]
`Readonly` **configFiles**: readonly `string`[]
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
*/
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,
NormalizedPlugin,
} 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';
export function isNxPluginV2(plugin: NxPlugin): plugin is NxPluginV2 {
@ -49,7 +54,7 @@ export function normalizeNxPlugin(plugin: NxPlugin): NormalizedPlugin {
}
export async function runCreateNodesInParallel(
configFiles: string[],
configFiles: readonly string[],
plugin: NormalizedPlugin,
options: unknown,
context: CreateNodesContext
@ -59,39 +64,33 @@ export async function runCreateNodesInParallel(
const errors: CreateNodesError[] = [];
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`);
// Result is either static or a promise, using Promise.resolve lets us
// handle both cases with same logic
const value = Promise.resolve(
plugin.createNodes[1](file, options, context)
);
return value
.catch((e) => {
performance.mark(`${plugin.name}:createNodes:${file} - end`);
errors.push(
new CreateNodesError({
error: e,
pluginName: plugin.name,
file,
})
);
return null;
})
.then((r) => {
performance.mark(`${plugin.name}:createNodes:${file} - end`);
performance.measure(
`${plugin.name}:createNodes:${file}`,
`${plugin.name}:createNodes:${file} - start`,
`${plugin.name}:createNodes:${file} - end`
);
// Existing behavior is to ignore null results of
// createNodes function.
if (r) {
results.push({ ...r, file, pluginName: plugin.name });
}
});
try {
const value = await plugin.createNodes[1](file, options, context);
if (value) {
results.push({
...value,
file,
pluginName: plugin.name,
});
}
} catch (e) {
errors.push(
new CreateNodesError({
error: e,
pluginName: plugin.name,
file,
})
);
} finally {
performance.mark(`${plugin.name}:createNodes:${file} - end`);
performance.measure(
`${plugin.name}:createNodes:${file}`,
`${plugin.name}:createNodes:${file} - start`,
`${plugin.name}:createNodes:${file} - end`
);
}
});
await Promise.all(promises).then(() => {