optional -> onPluginBootFailure
Signed-off-by: Tim Hansen <timbonicush@spotify.com>
This commit is contained in:
@@ -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
|
||||
```
|
||||
|
||||
Vendored
+13
-10
@@ -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,
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user