backend-app-api: settle all inits before exiting on error

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-09-19 12:03:45 +02:00
parent 332d4cb836
commit 04af116eea
3 changed files with 61 additions and 1 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/backend-app-api': patch
---
The backend will no longer exit immediately if any plugin or modules fails to initialize. Instead, the backend will wait for all plugins and modules to either start up successfully or throw, and then shut down the backend if there were any initialization errors.
This fixes an issue where backend initialization errors in adjacent plugins during database schema migration could cause the database migrations to be stuck in a locked state.
@@ -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,7 +275,7 @@ 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(
@@ -345,6 +345,16 @@ export class BackendInitializer {
}),
);
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();