diff --git a/.changeset/rich-deers-attend.md b/.changeset/rich-deers-attend.md new file mode 100644 index 0000000000..1b26636009 --- /dev/null +++ b/.changeset/rich-deers-attend.md @@ -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. diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts index b524301f21..8e92fc7120 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts @@ -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); diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index 6b020bb5ac..087f6e7924 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -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();