From 5622362b3eac0f750007b0afc7f9cf3eaf88484a Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Fri, 24 Jan 2025 13:59:17 -0700 Subject: [PATCH] Add configuration to permit backend startup failure Signed-off-by: Tim Hansen --- .changeset/twenty-carrots-jog.md | 17 +++++++++++++ packages/backend-app-api/config.d.ts | 4 +++ .../src/wiring/BackendInitializer.test.ts | 24 ++++++++++++++++++ .../src/wiring/BackendInitializer.ts | 25 ++++++++++++++++--- .../src/wiring/createInitializationLogger.ts | 8 ++++++ packages/backend-app-api/src/wiring/index.ts | 6 ++++- packages/backend-app-api/src/wiring/types.ts | 15 +++++++++++ 7 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 .changeset/twenty-carrots-jog.md diff --git a/.changeset/twenty-carrots-jog.md b/.changeset/twenty-carrots-jog.md new file mode 100644 index 0000000000..9a87c403ff --- /dev/null +++ b/.changeset/twenty-carrots-jog.md @@ -0,0 +1,17 @@ +--- +'@backstage/backend-app-api': patch +--- + +Added a configuration to permit backend plugin failures on startup: + +```yaml +backend: + ... + startup: + plugin-x: + optional: true +``` + +This configuration permits `plugin-x` to fail on startup. Omitting the `startup` +configuration matches the previous behavior, wherein any individual plugin +failure is fatal to backend startup. diff --git a/packages/backend-app-api/config.d.ts b/packages/backend-app-api/config.d.ts index b5e56af67c..7ffa882fc4 100644 --- a/packages/backend-app-api/config.d.ts +++ b/packages/backend-app-api/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { BackendStartupOptions } from './src'; + export interface Config { backend?: { /** Used by the feature discovery service */ @@ -23,5 +25,7 @@ export interface Config { include?: string[]; exclude?: string[]; }; + + startup?: BackendStartupOptions; }; } diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts index 39a54747d5..304b547d15 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts @@ -560,6 +560,30 @@ describe('BackendInitializer', () => { ); }); + it('should permit startup errors for plugins marked as optional', async () => { + const init = new BackendInitializer([ + mockServices.rootLifecycle.factory(), + mockServices.rootLogger.factory(), + mockServices.rootConfig.factory({ + data: { backend: { startup: { test: { optional: true } } } }, + }), + ]); + init.add( + createBackendPlugin({ + pluginId: 'test', + register(reg) { + reg.registerInit({ + deps: {}, + async init() { + throw new Error('NOPE'); + }, + }); + }, + }), + ); + await init.start(); + }); + it('should forward errors when multiple plugins fail to start', async () => { const init = new BackendInitializer([]); init.add( diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index 02fad4f217..eb63ebe78b 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -24,7 +24,7 @@ import { RootLifecycleService, createServiceFactory, } from '@backstage/backend-plugin-api'; -import { ServiceOrExtensionPoint } from './types'; +import { BackendStartupOptions, ServiceOrExtensionPoint } from './types'; // Direct internal import to avoid duplication // eslint-disable-next-line @backstage/no-relative-monorepo-imports import type { @@ -321,6 +321,21 @@ export class BackendInitializer { await this.#serviceRegistry.get(coreServices.rootLogger, 'root'), ); + const rootConfig = await this.#serviceRegistry.get( + coreServices.rootConfig, + 'root', + ); + + const startupOptions = + rootConfig + ?.getOptionalConfig('backend.startup') + ?.get() ?? {}; + + // Gather backend pluginIds marked as optional, since these should not cause a startup failure + const optionalBackends = Object.entries(startupOptions) + .filter(([_, value]) => value?.optional) + .map(([key]) => key); + // All plugins are initialized in parallel const results = await Promise.allSettled( allPluginIds.map(async pluginId => { @@ -392,8 +407,12 @@ export class BackendInitializer { await lifecycleService.startup(); } catch (error: unknown) { assertError(error); - initLogger.onPluginFailed(pluginId, error); - throw error; + if (optionalBackends.includes(pluginId)) { + initLogger.onOptionalPluginFailed(pluginId, error); + } else { + initLogger.onPluginFailed(pluginId, error); + throw error; + } } }), ); diff --git a/packages/backend-app-api/src/wiring/createInitializationLogger.ts b/packages/backend-app-api/src/wiring/createInitializationLogger.ts index e623d43074..9aea45c33f 100644 --- a/packages/backend-app-api/src/wiring/createInitializationLogger.ts +++ b/packages/backend-app-api/src/wiring/createInitializationLogger.ts @@ -28,6 +28,7 @@ export function createInitializationLogger( ): { onPluginStarted(pluginId: string): void; onPluginFailed(pluginId: string, error: Error): void; + onOptionalPluginFailed(pluginId: string, error: Error): void; onAllStarted(): void; } { const logger = rootLogger?.child({ type: 'initialization' }); @@ -79,6 +80,13 @@ export function createInitializationLogger( error, ); }, + onOptionalPluginFailed(pluginId: string, error: Error) { + starting.delete(pluginId); + logger?.error( + `Plugin '${pluginId}' threw an error during startup, but the plugin is marked optional in config so startup will continue.`, + error, + ); + }, onAllStarted() { logger?.info(`Plugin initialization complete${getInitStatus()}`); diff --git a/packages/backend-app-api/src/wiring/index.ts b/packages/backend-app-api/src/wiring/index.ts index 00e4a9d4b4..0479ba32ef 100644 --- a/packages/backend-app-api/src/wiring/index.ts +++ b/packages/backend-app-api/src/wiring/index.ts @@ -14,5 +14,9 @@ * limitations under the License. */ -export type { Backend, CreateSpecializedBackendOptions } from './types'; +export type { + Backend, + BackendStartupOptions, + CreateSpecializedBackendOptions, +} from './types'; export { createSpecializedBackend } from './createSpecializedBackend'; diff --git a/packages/backend-app-api/src/wiring/types.ts b/packages/backend-app-api/src/wiring/types.ts index bd761ff3c8..0b19ec66ea 100644 --- a/packages/backend-app-api/src/wiring/types.ts +++ b/packages/backend-app-api/src/wiring/types.ts @@ -43,3 +43,18 @@ export interface CreateSpecializedBackendOptions { export type ServiceOrExtensionPoint = | ExtensionPoint | ServiceRef; + +/** + * @public + */ +export type BackendStartupOptions = { + [pluginId: string]: { + /** + * Used to mark plugins as optional, which allows the backend to start up even in the event + * of a plugin failure. Plugin failures without this configuration are fatal. This can + * enable leaving a crashing plugin installed, but still permit backend startup, which may + * help troubleshoot data-dependent issues. + */ + optional?: boolean; + }; +};