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:
@@ -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()}`);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user