optional -> onPluginBootFailure

Signed-off-by: Tim Hansen <timbonicush@spotify.com>
This commit is contained in:
Tim Hansen
2025-02-13 08:32:30 -07:00
parent 1d7a9277f4
commit 667fdb6766
5 changed files with 50 additions and 38 deletions
+7 -7
View File
@@ -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
```
+13 -10
View File
@@ -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';
};
};
};
@@ -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' } },
},
},
},
@@ -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';
}
}
@@ -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,
);
},