Add default startup options

Signed-off-by: Tim Hansen <timbonicush@spotify.com>
This commit is contained in:
Tim Hansen
2025-02-07 13:57:07 -07:00
parent 5622362b3e
commit c5b6f34ec7
5 changed files with 123 additions and 25 deletions
@@ -565,7 +565,9 @@ describe('BackendInitializer', () => {
mockServices.rootLifecycle.factory(),
mockServices.rootLogger.factory(),
mockServices.rootConfig.factory({
data: { backend: { startup: { test: { optional: true } } } },
data: {
backend: { startup: { plugins: { test: { optional: true } } } },
},
}),
]);
init.add(
@@ -584,6 +586,63 @@ describe('BackendInitializer', () => {
await init.start();
});
it('should permit startup errors if the default is set', async () => {
const init = new BackendInitializer([
mockServices.rootLifecycle.factory(),
mockServices.rootLogger.factory(),
mockServices.rootConfig.factory({
data: { backend: { startup: { default: { 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 for plugins explicitly marked as not optional when the default is true', async () => {
const init = new BackendInitializer([
mockServices.rootLifecycle.factory(),
mockServices.rootLogger.factory(),
mockServices.rootConfig.factory({
data: {
backend: {
startup: {
default: { optional: true },
plugins: { test: { optional: false } },
},
},
},
}),
]);
init.add(
createBackendPlugin({
pluginId: 'test',
register(reg) {
reg.registerInit({
deps: {},
async init() {
throw new Error('NOPE');
},
});
},
}),
);
await expect(init.start()).rejects.toThrow(
"Plugin 'test' startup failed; caused by Error: NOPE",
);
});
it('should forward errors when multiple plugins fail to start', async () => {
const init = new BackendInitializer([]);
init.add(
@@ -24,7 +24,8 @@ import {
RootLifecycleService,
createServiceFactory,
} from '@backstage/backend-plugin-api';
import { BackendStartupOptions, ServiceOrExtensionPoint } from './types';
import { Config } from '@backstage/config';
import { ServiceOrExtensionPoint } from './types';
// Direct internal import to avoid duplication
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import type {
@@ -326,19 +327,14 @@ export class BackendInitializer {
'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 => {
const isPluginOptional = this.#getPluginOptionalityPredicate(
pluginId,
rootConfig,
);
try {
// Initialize all eager services
await this.#serviceRegistry.initializeEagerServicesWithScope(
@@ -407,7 +403,7 @@ export class BackendInitializer {
await lifecycleService.startup();
} catch (error: unknown) {
assertError(error);
if (optionalBackends.includes(pluginId)) {
if (isPluginOptional) {
initLogger.onOptionalPluginFailed(pluginId, error);
} else {
initLogger.onPluginFailed(pluginId, error);
@@ -639,6 +635,17 @@ export class BackendInitializer {
}
}
}
#getPluginOptionalityPredicate(pluginId: string, config?: Config): boolean {
const defaultStartupOptionalValue =
config?.getOptionalBoolean('backend.startup.default.optional') ?? false;
return (
config?.getOptionalBoolean(
`backend.startup.plugins.${pluginId}.optional`,
) ?? defaultStartupOptionalValue
);
}
}
function toInternalBackendFeature(
+6 -7
View File
@@ -48,13 +48,12 @@ export type ServiceOrExtensionPoint<T = unknown> =
* @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.
*/
default?: {
optional?: boolean;
};
plugins?: {
[pluginId: string]: {
optional?: boolean;
};
};
};