Merge pull request #26787 from backstage/rugvip/wait
backend-app-api: settle all inits before exiting on error
This commit is contained in:
@@ -418,6 +418,49 @@ describe('BackendInitializer', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should forward errors when multiple plugins fail to start', async () => {
|
||||
const init = new BackendInitializer([]);
|
||||
init.add(
|
||||
createBackendPlugin({
|
||||
pluginId: 'test-1',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {
|
||||
throw new Error('NOPE A');
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
init.add(
|
||||
createBackendPlugin({
|
||||
pluginId: 'test-2',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {
|
||||
throw new Error('NOPE B');
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
const result = init.start();
|
||||
|
||||
await expect(result).rejects.toThrow('Backend startup failed');
|
||||
await expect(result).rejects.toMatchObject({
|
||||
errors: [
|
||||
expect.objectContaining({
|
||||
message: "Plugin 'test-1' startup failed; caused by Error: NOPE A",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
message: "Plugin 'test-2' startup failed; caused by Error: NOPE B",
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should forward errors when modules fail to start', async () => {
|
||||
const init = new BackendInitializer([]);
|
||||
init.add(testPlugin);
|
||||
|
||||
@@ -275,76 +275,91 @@ export class BackendInitializer {
|
||||
);
|
||||
|
||||
// All plugins are initialized in parallel
|
||||
await Promise.all(
|
||||
const results = await Promise.allSettled(
|
||||
allPluginIds.map(async pluginId => {
|
||||
// Initialize all eager services
|
||||
await this.#serviceRegistry.initializeEagerServicesWithScope(
|
||||
'plugin',
|
||||
pluginId,
|
||||
);
|
||||
|
||||
// Modules are initialized before plugins, so that they can provide extension to the plugin
|
||||
const modules = moduleInits.get(pluginId);
|
||||
if (modules) {
|
||||
const tree = DependencyGraph.fromIterable(
|
||||
Array.from(modules).map(([moduleId, moduleInit]) => ({
|
||||
value: { moduleId, moduleInit },
|
||||
// Relationships are reversed at this point since we're only interested in the extension points.
|
||||
// If a modules provides extension point A we want it to be initialized AFTER all modules
|
||||
// that depend on extension point A, so that they can provide their extensions.
|
||||
consumes: Array.from(moduleInit.provides).map(p => p.id),
|
||||
provides: Array.from(moduleInit.consumes).map(c => c.id),
|
||||
})),
|
||||
);
|
||||
const circular = tree.detectCircularDependency();
|
||||
if (circular) {
|
||||
throw new ConflictError(
|
||||
`Circular dependency detected for modules of plugin '${pluginId}', ${circular
|
||||
.map(({ moduleId }) => `'${moduleId}'`)
|
||||
.join(' -> ')}`,
|
||||
);
|
||||
}
|
||||
await tree.parallelTopologicalTraversal(
|
||||
async ({ moduleId, moduleInit }) => {
|
||||
const moduleDeps = await this.#getInitDeps(
|
||||
moduleInit.init.deps,
|
||||
pluginId,
|
||||
moduleId,
|
||||
);
|
||||
await moduleInit.init.func(moduleDeps).catch(error => {
|
||||
throw new ForwardedError(
|
||||
`Module '${moduleId}' for plugin '${pluginId}' startup failed`,
|
||||
error,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Once all modules have been initialized, we can initialize the plugin itself
|
||||
const pluginInit = pluginInits.get(pluginId);
|
||||
// We allow modules to be installed without the accompanying plugin, so the plugin may not exist
|
||||
if (pluginInit) {
|
||||
const pluginDeps = await this.#getInitDeps(
|
||||
pluginInit.init.deps,
|
||||
try {
|
||||
// Initialize all eager services
|
||||
await this.#serviceRegistry.initializeEagerServicesWithScope(
|
||||
'plugin',
|
||||
pluginId,
|
||||
);
|
||||
await pluginInit.init.func(pluginDeps).catch(error => {
|
||||
throw new ForwardedError(
|
||||
`Plugin '${pluginId}' startup failed`,
|
||||
error,
|
||||
|
||||
// Modules are initialized before plugins, so that they can provide extension to the plugin
|
||||
const modules = moduleInits.get(pluginId);
|
||||
if (modules) {
|
||||
const tree = DependencyGraph.fromIterable(
|
||||
Array.from(modules).map(([moduleId, moduleInit]) => ({
|
||||
value: { moduleId, moduleInit },
|
||||
// Relationships are reversed at this point since we're only interested in the extension points.
|
||||
// If a modules provides extension point A we want it to be initialized AFTER all modules
|
||||
// that depend on extension point A, so that they can provide their extensions.
|
||||
consumes: Array.from(moduleInit.provides).map(p => p.id),
|
||||
provides: Array.from(moduleInit.consumes).map(c => c.id),
|
||||
})),
|
||||
);
|
||||
});
|
||||
const circular = tree.detectCircularDependency();
|
||||
if (circular) {
|
||||
throw new ConflictError(
|
||||
`Circular dependency detected for modules of plugin '${pluginId}', ${circular
|
||||
.map(({ moduleId }) => `'${moduleId}'`)
|
||||
.join(' -> ')}`,
|
||||
);
|
||||
}
|
||||
await tree.parallelTopologicalTraversal(
|
||||
async ({ moduleId, moduleInit }) => {
|
||||
const moduleDeps = await this.#getInitDeps(
|
||||
moduleInit.init.deps,
|
||||
pluginId,
|
||||
moduleId,
|
||||
);
|
||||
await moduleInit.init.func(moduleDeps).catch(error => {
|
||||
throw new ForwardedError(
|
||||
`Module '${moduleId}' for plugin '${pluginId}' startup failed`,
|
||||
error,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Once all modules have been initialized, we can initialize the plugin itself
|
||||
const pluginInit = pluginInits.get(pluginId);
|
||||
// We allow modules to be installed without the accompanying plugin, so the plugin may not exist
|
||||
if (pluginInit) {
|
||||
const pluginDeps = await this.#getInitDeps(
|
||||
pluginInit.init.deps,
|
||||
pluginId,
|
||||
);
|
||||
await pluginInit.init.func(pluginDeps).catch(error => {
|
||||
throw new ForwardedError(
|
||||
`Plugin '${pluginId}' startup failed`,
|
||||
error,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
initLogger.onPluginStarted(pluginId);
|
||||
|
||||
// Once the plugin and all modules have been initialized, we can signal that the plugin has stared up successfully
|
||||
const lifecycleService = await this.#getPluginLifecycleImpl(pluginId);
|
||||
await lifecycleService.startup();
|
||||
} catch (error) {
|
||||
initLogger.onPluginFailed(pluginId);
|
||||
throw error;
|
||||
}
|
||||
|
||||
initLogger.onPluginStarted(pluginId);
|
||||
|
||||
// Once the plugin and all modules have been initialized, we can signal that the plugin has stared up successfully
|
||||
const lifecycleService = await this.#getPluginLifecycleImpl(pluginId);
|
||||
await lifecycleService.startup();
|
||||
}),
|
||||
);
|
||||
|
||||
const initErrors = results.flatMap(r =>
|
||||
r.status === 'rejected' ? [r.reason] : [],
|
||||
);
|
||||
if (initErrors.length === 1) {
|
||||
throw initErrors[0];
|
||||
} else if (initErrors.length > 1) {
|
||||
// TODO(Rugvip): Seems like there aren't proper types for AggregateError yet
|
||||
throw new (AggregateError as any)(initErrors, 'Backend startup failed');
|
||||
}
|
||||
|
||||
// Once all plugins and modules have been initialized, we can signal that the backend has started up successfully
|
||||
const lifecycleService = await this.#getRootLifecycleImpl();
|
||||
await lifecycleService.startup();
|
||||
|
||||
@@ -27,6 +27,7 @@ export function createInitializationLogger(
|
||||
rootLogger?: RootLoggerService,
|
||||
): {
|
||||
onPluginStarted(pluginId: string): void;
|
||||
onPluginFailed(pluginId: string): void;
|
||||
onAllStarted(): void;
|
||||
} {
|
||||
const logger = rootLogger?.child({ type: 'initialization' });
|
||||
@@ -67,6 +68,16 @@ export function createInitializationLogger(
|
||||
starting.delete(pluginId);
|
||||
started.add(pluginId);
|
||||
},
|
||||
onPluginFailed(pluginId: string) {
|
||||
starting.delete(pluginId);
|
||||
const status =
|
||||
starting.size > 0
|
||||
? `, waiting for ${starting.size} other plugins to finish before shutting down the process`
|
||||
: '';
|
||||
logger?.error(
|
||||
`Plugin '${pluginId}' thew an error during startup${status}`,
|
||||
);
|
||||
},
|
||||
onAllStarted() {
|
||||
logger?.info(`Plugin initialization complete${getInitStatus()}`);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user