diff --git a/.changeset/twenty-carrots-jog.md b/.changeset/twenty-carrots-jog.md index 71858ccd11..f4b4db8fcb 100644 --- a/.changeset/twenty-carrots-jog.md +++ b/.changeset/twenty-carrots-jog.md @@ -10,22 +10,22 @@ backend: startup: plugins: plugin-x: - optional: true + onPluginBootFailure: continue ``` -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. +This configuration permits `plugin-x` to fail on startup. Omitting the +`onPluginBootFailure` configuration matches the previous behavior, wherein any +individual plugin failure aborts backend startup. -The default can also be changed, so that all plugins are considered optional +The default can also be changed, so that continuing on failure is the default unless otherwise specified: ```yaml backend: startup: default: - optional: true + onPluginBootFailure: continue plugins: catalog: - optional: false + onPluginBootFailure: abort ``` diff --git a/packages/backend-app-api/config.d.ts b/packages/backend-app-api/config.d.ts index 8af778ca42..0cf91852a1 100644 --- a/packages/backend-app-api/config.d.ts +++ b/packages/backend-app-api/config.d.ts @@ -27,22 +27,25 @@ export interface Config { startup?: { default?: { /** - * The default value for `optional` if not specified for a particular plugin. This defaults to - * false, which means `optional: true` must be specified for individual plugins to be considered - * optional. This can also be set to true, which flips the logic for individual plugins so that - * they must be set to `optional: false` to be required. + * The default value for `onPluginBootFailure` if not specified for a particular plugin. + * This defaults to 'abort', which means `onPluginBootFailure: continue` must be specified + * for backend startup to continue on plugin boot failure. This can also be set to + * 'continue', which flips the logic for individual plugins so that they must be set to + * `onPluginBootFailure: abort` to be required. */ - optional?: boolean; + onPluginBootFailure?: 'continue' | 'abort'; }; plugins?: { [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. + * Used to control backend startup behavior when this plugin fails to boot up. Setting + * this to `continue` allows the backend to continue starting up, even if this plugin + * fails. This can enable leaving a crashing plugin installed, but still permit backend + * startup, which may help troubleshoot data-dependent issues. Plugin failures for plugins + * set to `abort` are fatal (this is the default unless overridden by the `default` + * setting). */ - optional?: boolean; + onPluginBootFailure?: 'continue' | 'abort'; }; }; }; diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts index 4edfc7275f..fe0a4f526b 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts @@ -560,13 +560,15 @@ describe('BackendInitializer', () => { ); }); - it('should permit startup errors for plugins marked as optional', async () => { + it('should permit startup errors for plugins with onPluginBootFailure: continue', async () => { const init = new BackendInitializer([ mockServices.rootLifecycle.factory(), mockServices.rootLogger.factory(), mockServices.rootConfig.factory({ data: { - backend: { startup: { plugins: { test: { optional: true } } } }, + backend: { + startup: { plugins: { test: { onPluginBootFailure: 'continue' } } }, + }, }, }), ]); @@ -586,12 +588,16 @@ describe('BackendInitializer', () => { await expect(init.start()).resolves.not.toThrow(); }); - it('should permit startup errors if the default is set', async () => { + it('should permit startup errors if the default onPluginBootFailure is continue', async () => { const init = new BackendInitializer([ mockServices.rootLifecycle.factory(), mockServices.rootLogger.factory(), mockServices.rootConfig.factory({ - data: { backend: { startup: { default: { optional: true } } } }, + data: { + backend: { + startup: { default: { onPluginBootFailure: 'continue' } }, + }, + }, }), ]); init.add( @@ -610,7 +616,7 @@ describe('BackendInitializer', () => { await expect(init.start()).resolves.not.toThrow(); }); - it('should forward errors for plugins explicitly marked as not optional when the default is true', async () => { + it('should forward errors for plugins explicitly marked to abort when the default is continue', async () => { const init = new BackendInitializer([ mockServices.rootLifecycle.factory(), mockServices.rootLogger.factory(), @@ -618,8 +624,8 @@ describe('BackendInitializer', () => { data: { backend: { startup: { - default: { optional: true }, - plugins: { test: { optional: false } }, + default: { onPluginBootFailure: 'continue' }, + plugins: { test: { onPluginBootFailure: 'abort' } }, }, }, }, diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index 5384423bfe..f521966d4d 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -330,7 +330,7 @@ export class BackendInitializer { // All plugins are initialized in parallel const results = await Promise.allSettled( allPluginIds.map(async pluginId => { - const isPluginOptional = this.#getPluginOptionalityPredicate( + const isBootFailurePermitted = this.#getPluginBootFailurePredicate( pluginId, rootConfig, ); @@ -403,8 +403,8 @@ export class BackendInitializer { await lifecycleService.startup(); } catch (error: unknown) { assertError(error); - if (isPluginOptional) { - initLogger.onOptionalPluginFailed(pluginId, error); + if (isBootFailurePermitted) { + initLogger.onPermittedPluginFailure(pluginId, error); } else { initLogger.onPluginFailed(pluginId, error); throw error; @@ -636,15 +636,18 @@ export class BackendInitializer { } } - #getPluginOptionalityPredicate(pluginId: string, config?: Config): boolean { - const defaultStartupOptionalValue = - config?.getOptionalBoolean('backend.startup.default.optional') ?? false; + #getPluginBootFailurePredicate(pluginId: string, config?: Config): boolean { + const defaultStartupBootFailureValue = + config?.getOptionalString( + 'backend.startup.default.onPluginBootFailure', + ) ?? 'abort'; - return ( - config?.getOptionalBoolean( - `backend.startup.plugins.${pluginId}.optional`, - ) ?? defaultStartupOptionalValue - ); + const pluginStartupBootFailureValue = + config?.getOptionalString( + `backend.startup.plugins.${pluginId}.onPluginBootFailure`, + ) ?? defaultStartupBootFailureValue; + + return pluginStartupBootFailureValue === 'continue'; } } diff --git a/packages/backend-app-api/src/wiring/createInitializationLogger.ts b/packages/backend-app-api/src/wiring/createInitializationLogger.ts index 9aea45c33f..0978cfb0c8 100644 --- a/packages/backend-app-api/src/wiring/createInitializationLogger.ts +++ b/packages/backend-app-api/src/wiring/createInitializationLogger.ts @@ -28,7 +28,7 @@ export function createInitializationLogger( ): { onPluginStarted(pluginId: string): void; onPluginFailed(pluginId: string, error: Error): void; - onOptionalPluginFailed(pluginId: string, error: Error): void; + onPermittedPluginFailure(pluginId: string, error: Error): void; onAllStarted(): void; } { const logger = rootLogger?.child({ type: 'initialization' }); @@ -80,10 +80,10 @@ export function createInitializationLogger( error, ); }, - onOptionalPluginFailed(pluginId: string, error: Error) { + onPermittedPluginFailure(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.`, + `Plugin '${pluginId}' threw an error during startup, but boot failure is permitted for this plugin so startup will continue.`, error, ); },