Add configuration to permit backend startup failure
Signed-off-by: Tim Hansen <timbonicush@spotify.com>
This commit is contained in:
@@ -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.
|
||||
Vendored
+4
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<BackendStartupOptions>() ?? {};
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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()}`);
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -43,3 +43,18 @@ export interface CreateSpecializedBackendOptions {
|
||||
export type ServiceOrExtensionPoint<T = unknown> =
|
||||
| ExtensionPoint<T>
|
||||
| ServiceRef<T>;
|
||||
|
||||
/**
|
||||
* @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;
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user