Merge pull request #29348 from webark/feat/allowing-for-module-boot-failure

feat(backend-app-api): add config to permit module failures on startup to not effect the plugin from starting up
This commit is contained in:
Ben Lambert
2025-04-29 09:37:35 +02:00
committed by GitHub
5 changed files with 259 additions and 17 deletions
@@ -541,7 +541,7 @@ describe('BackendInitializer', () => {
});
it('should forward errors when plugins fail to start', async () => {
const init = new BackendInitializer([]);
const init = new BackendInitializer(baseFactories);
init.add(
createBackendPlugin({
pluginId: 'test',
@@ -562,8 +562,7 @@ describe('BackendInitializer', () => {
it('should permit startup errors for plugins with onPluginBootFailure: continue', async () => {
const init = new BackendInitializer([
mockServices.rootLifecycle.factory(),
mockServices.rootLogger.factory(),
...baseFactories,
mockServices.rootConfig.factory({
data: {
backend: {
@@ -590,8 +589,7 @@ describe('BackendInitializer', () => {
it('should permit startup errors if the default onPluginBootFailure is continue', async () => {
const init = new BackendInitializer([
mockServices.rootLifecycle.factory(),
mockServices.rootLogger.factory(),
...baseFactories,
mockServices.rootConfig.factory({
data: {
backend: {
@@ -618,8 +616,7 @@ describe('BackendInitializer', () => {
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(),
...baseFactories,
mockServices.rootConfig.factory({
data: {
backend: {
@@ -649,6 +646,130 @@ describe('BackendInitializer', () => {
);
});
it('should forward errors when plugin modules fail to start', async () => {
const init = new BackendInitializer(baseFactories);
init.add(testPlugin);
init.add(
createBackendModule({
pluginId: 'test',
moduleId: 'mod',
register(reg) {
reg.registerInit({
deps: {},
async init() {
throw new Error('NOPE');
},
});
},
}),
);
await expect(init.start()).rejects.toThrow(
"Module 'mod' for plugin 'test' startup failed; caused by Error: NOPE",
);
});
it('should permit startup errors for plugin modules with onPluginModuleBootFailure: continue', async () => {
const init = new BackendInitializer([
...baseFactories,
mockServices.rootConfig.factory({
data: {
backend: {
startup: {
plugins: {
test: {
modules: { mod: { onPluginModuleBootFailure: 'continue' } },
},
},
},
},
},
}),
]);
init.add(testPlugin);
init.add(
createBackendModule({
pluginId: 'test',
moduleId: 'mod',
register(reg) {
reg.registerInit({
deps: {},
async init() {
throw new Error('NOPE');
},
});
},
}),
);
await expect(init.start()).resolves.not.toThrow();
});
it('should permit startup errors if the default onPluginModuleBootFailure is continue', async () => {
const init = new BackendInitializer([
...baseFactories,
mockServices.rootConfig.factory({
data: {
backend: {
startup: { default: { onPluginModuleBootFailure: 'continue' } },
},
},
}),
]);
init.add(testPlugin);
init.add(
createBackendModule({
pluginId: 'test',
moduleId: 'mod',
register(reg) {
reg.registerInit({
deps: {},
async init() {
throw new Error('NOPE');
},
});
},
}),
);
await expect(init.start()).resolves.not.toThrow();
});
it('should forward errors for plugin modules explicitly marked to abort when the default is continue', async () => {
const init = new BackendInitializer([
...baseFactories,
mockServices.rootConfig.factory({
data: {
backend: {
startup: {
default: { onPluginModuleBootFailure: 'continue' },
plugins: {
test: {
modules: { mod: { onPluginModuleBootFailure: 'abort' } },
},
},
},
},
},
}),
]);
init.add(testPlugin);
init.add(
createBackendModule({
pluginId: 'test',
moduleId: 'mod',
register(reg) {
reg.registerInit({
deps: {},
async init() {
throw new Error('NOPE');
},
});
},
}),
);
await expect(init.start()).rejects.toThrow(
"Module 'mod' for 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(
@@ -365,17 +365,38 @@ export class BackendInitializer {
}
await tree.parallelTopologicalTraversal(
async ({ moduleId, moduleInit }) => {
const moduleDeps = await this.#getInitDeps(
moduleInit.init.deps,
pluginId,
moduleId,
);
await moduleInit.init.func(moduleDeps).catch(error => {
throw new ForwardedError(
`Module '${moduleId}' for plugin '${pluginId}' startup failed`,
error,
const isModuleBootFailurePermitted =
this.#getPluginModuleBootFailurePredicate(
pluginId,
moduleId,
rootConfig,
);
});
try {
const moduleDeps = await this.#getInitDeps(
moduleInit.init.deps,
pluginId,
moduleId,
);
await moduleInit.init.func(moduleDeps).catch(error => {
throw new ForwardedError(
`Module '${moduleId}' for plugin '${pluginId}' startup failed`,
error,
);
});
} catch (error: unknown) {
assertError(error);
if (isModuleBootFailurePermitted) {
initLogger.onPermittedPluginModuleFailure(
pluginId,
moduleId,
error,
);
} else {
initLogger.onPluginModuleFailed(pluginId, moduleId, error);
throw error;
}
}
},
);
}
@@ -649,6 +670,24 @@ export class BackendInitializer {
return pluginStartupBootFailureValue === 'continue';
}
#getPluginModuleBootFailurePredicate(
pluginId: string,
moduleId: string,
config?: Config,
): boolean {
const defaultStartupBootFailureValue =
config?.getOptionalString(
'backend.startup.default.onPluginModuleBootFailure',
) ?? 'abort';
const pluginModuleStartupBootFailureValue =
config?.getOptionalString(
`backend.startup.plugins.${pluginId}.modules.${moduleId}.onPluginModuleBootFailure`,
) ?? defaultStartupBootFailureValue;
return pluginModuleStartupBootFailureValue === 'continue';
}
}
function toInternalBackendFeature(
@@ -29,6 +29,12 @@ export function createInitializationLogger(
onPluginStarted(pluginId: string): void;
onPluginFailed(pluginId: string, error: Error): void;
onPermittedPluginFailure(pluginId: string, error: Error): void;
onPluginModuleFailed(pluginId: string, moduleId: string, error: Error): void;
onPermittedPluginModuleFailure(
pluginId: string,
moduleId: string,
error: Error,
): void;
onAllStarted(): void;
} {
const logger = rootLogger?.child({ type: 'initialization' });
@@ -87,6 +93,26 @@ export function createInitializationLogger(
error,
);
},
onPluginModuleFailed(pluginId: string, moduleId: string, error: Error) {
const status =
starting.size > 0
? `, waiting for ${starting.size} other plugins to finish before shutting down the process`
: '';
logger?.error(
`Module ${moduleId} in Plugin '${pluginId}' threw an error during startup${status}.`,
error,
);
},
onPermittedPluginModuleFailure(
pluginId: string,
moduleId: string,
error: Error,
) {
logger?.error(
`Module ${moduleId} in Plugin '${pluginId}' threw an error during startup, but boot failure is permitted for this plugin module so startup will continue.`,
error,
);
},
onAllStarted() {
logger?.info(`Plugin initialization complete${getInitStatus()}`);