Merge pull request #26787 from backstage/rugvip/wait

backend-app-api: settle all inits before exiting on error
This commit is contained in:
Patrik Oldsberg
2024-09-20 07:49:37 +02:00
committed by GitHub
4 changed files with 138 additions and 62 deletions
@@ -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()}`);