backend-app-api: refactor startup tracking and report results
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { BackendInitializer } from './BackendInitializer';
|
||||
import { mockServices } from '@backstage/backend-test-utils';
|
||||
import { BackendStartupError } from './BackendStartupError';
|
||||
|
||||
const baseFactories = [
|
||||
mockServices.rootLifecycle.factory(),
|
||||
@@ -806,49 +807,6 @@ describe('BackendInitializer', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should forward errors when multiple plugins fail to start', async () => {
|
||||
const init = new BackendInitializer([]);
|
||||
init.add(
|
||||
createBackendPlugin({
|
||||
pluginId: 'test-1',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {
|
||||
throw new Error('NOPE A');
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
init.add(
|
||||
createBackendPlugin({
|
||||
pluginId: 'test-2',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {
|
||||
throw new Error('NOPE B');
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
const result = init.start();
|
||||
|
||||
await expect(result).rejects.toThrow('Backend startup failed');
|
||||
await expect(result).rejects.toMatchObject({
|
||||
errors: [
|
||||
expect.objectContaining({
|
||||
message: "Plugin 'test-1' startup failed; caused by Error: NOPE A",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
message: "Plugin 'test-2' startup failed; caused by Error: NOPE B",
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should forward errors when modules fail to start', async () => {
|
||||
const init = new BackendInitializer([]);
|
||||
init.add(testPlugin);
|
||||
@@ -1289,4 +1247,594 @@ describe('BackendInitializer', () => {
|
||||
await backend.add(slowConsumerModule);
|
||||
await backend.start();
|
||||
});
|
||||
|
||||
describe('startup results', () => {
|
||||
it('should return successful startup result when all plugins and modules start successfully', async () => {
|
||||
const init = new BackendInitializer(baseFactories);
|
||||
init.add(
|
||||
createBackendPlugin({
|
||||
pluginId: 'plugin1',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
init.add(
|
||||
createBackendPlugin({
|
||||
pluginId: 'plugin2',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
init.add(
|
||||
createBackendModule({
|
||||
pluginId: 'plugin1',
|
||||
moduleId: 'module1',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
init.add(
|
||||
createBackendModule({
|
||||
pluginId: 'plugin2',
|
||||
moduleId: 'module2',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await init.start();
|
||||
|
||||
expect(result.plugins).toHaveLength(2);
|
||||
expect(result.plugins[0]).toMatchObject({
|
||||
pluginId: 'plugin1',
|
||||
modules: [
|
||||
{
|
||||
moduleId: 'module1',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(result.plugins[0].failure).toBeUndefined();
|
||||
expect(result.plugins[0].modules[0].failure).toBeUndefined();
|
||||
expect(result.plugins[1]).toMatchObject({
|
||||
pluginId: 'plugin2',
|
||||
modules: [
|
||||
{
|
||||
moduleId: 'module2',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(result.plugins[1].failure).toBeUndefined();
|
||||
expect(result.plugins[1].modules[0].failure).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw BackendStartupError with results when plugin fails to start', async () => {
|
||||
const init = new BackendInitializer(baseFactories);
|
||||
const error = new Error('Plugin failed');
|
||||
init.add(
|
||||
createBackendPlugin({
|
||||
pluginId: 'plugin1',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {
|
||||
throw error;
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
init.add(
|
||||
createBackendPlugin({
|
||||
pluginId: 'plugin2',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const err = await init.start().then(
|
||||
() => {
|
||||
throw new Error('Expected BackendStartupError to be thrown');
|
||||
},
|
||||
(e: BackendStartupError) => e,
|
||||
);
|
||||
|
||||
expect(err).toBeInstanceOf(BackendStartupError);
|
||||
expect(err?.result.plugins).toHaveLength(2);
|
||||
expect(err?.result.plugins[0]).toMatchObject({
|
||||
pluginId: 'plugin1',
|
||||
failure: { error, allowed: false },
|
||||
});
|
||||
expect(err?.result.plugins[1]).toMatchObject({
|
||||
pluginId: 'plugin2',
|
||||
});
|
||||
expect(err?.result.plugins[1].failure).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw BackendStartupError with results when module fails to start', async () => {
|
||||
const init = new BackendInitializer(baseFactories);
|
||||
const error = new Error('Module failed');
|
||||
init.add(
|
||||
createBackendPlugin({
|
||||
pluginId: 'plugin1',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
init.add(
|
||||
createBackendModule({
|
||||
pluginId: 'plugin1',
|
||||
moduleId: 'module1',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {
|
||||
throw error;
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const err = await init.start().then(
|
||||
() => {
|
||||
throw new Error('Expected BackendStartupError to be thrown');
|
||||
},
|
||||
(e: BackendStartupError) => e,
|
||||
);
|
||||
|
||||
expect(err).toBeInstanceOf(BackendStartupError);
|
||||
expect(err?.result.plugins).toHaveLength(1);
|
||||
expect(err?.result.plugins[0]).toMatchObject({
|
||||
pluginId: 'plugin1',
|
||||
modules: [
|
||||
{
|
||||
moduleId: 'module1',
|
||||
failure: { error, allowed: false },
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(err?.result.plugins[0].failure).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw BackendStartupError with results when multiple plugins fail', async () => {
|
||||
const init = new BackendInitializer(baseFactories);
|
||||
const error1 = new Error('Plugin1 failed');
|
||||
const error2 = new Error('Plugin2 failed');
|
||||
init.add(
|
||||
createBackendPlugin({
|
||||
pluginId: 'plugin1',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {
|
||||
throw error1;
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
init.add(
|
||||
createBackendPlugin({
|
||||
pluginId: 'plugin2',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {
|
||||
throw error2;
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const err = await init.start().then(
|
||||
() => {
|
||||
throw new Error('Expected BackendStartupError to be thrown');
|
||||
},
|
||||
(e: BackendStartupError) => e,
|
||||
);
|
||||
|
||||
expect(err).toBeInstanceOf(BackendStartupError);
|
||||
expect(err?.result.plugins).toHaveLength(2);
|
||||
expect(err?.result.plugins[0].failure?.error).toBe(error1);
|
||||
expect(err?.result.plugins[1].failure?.error).toBe(error2);
|
||||
});
|
||||
|
||||
it('should return results with failure status when plugin boot failure is permitted', async () => {
|
||||
const error = new Error('Plugin failed');
|
||||
const init = new BackendInitializer([
|
||||
...baseFactories,
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
backend: {
|
||||
startup: {
|
||||
plugins: { plugin1: { onPluginBootFailure: 'continue' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
init.add(
|
||||
createBackendPlugin({
|
||||
pluginId: 'plugin1',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {
|
||||
throw error;
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
init.add(
|
||||
createBackendPlugin({
|
||||
pluginId: 'plugin2',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await init.start();
|
||||
|
||||
expect(result.plugins).toHaveLength(2);
|
||||
expect(result.plugins[0]).toMatchObject({
|
||||
pluginId: 'plugin1',
|
||||
failure: { error, allowed: true },
|
||||
});
|
||||
expect(result.plugins[1]).toMatchObject({
|
||||
pluginId: 'plugin2',
|
||||
});
|
||||
expect(result.plugins[1].failure).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return results with failure status when module boot failure is permitted', async () => {
|
||||
const error = new Error('Module failed');
|
||||
const init = new BackendInitializer([
|
||||
...baseFactories,
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
backend: {
|
||||
startup: {
|
||||
plugins: {
|
||||
plugin1: {
|
||||
modules: {
|
||||
module1: { onPluginModuleBootFailure: 'continue' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
init.add(
|
||||
createBackendPlugin({
|
||||
pluginId: 'plugin1',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
init.add(
|
||||
createBackendModule({
|
||||
pluginId: 'plugin1',
|
||||
moduleId: 'module1',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {
|
||||
throw error;
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
init.add(
|
||||
createBackendModule({
|
||||
pluginId: 'plugin1',
|
||||
moduleId: 'module2',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await init.start();
|
||||
|
||||
expect(result.plugins).toHaveLength(1);
|
||||
expect(result.plugins[0]).toMatchObject({
|
||||
pluginId: 'plugin1',
|
||||
modules: [
|
||||
{
|
||||
moduleId: 'module1',
|
||||
failure: { error, allowed: true },
|
||||
},
|
||||
{
|
||||
moduleId: 'module2',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(result.plugins[0].failure).toBeUndefined();
|
||||
expect(result.plugins[0].modules[1].failure).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should include all module results even when some modules fail', async () => {
|
||||
const error1 = new Error('Module1 failed');
|
||||
const error2 = new Error('Module2 failed');
|
||||
const init = new BackendInitializer([
|
||||
...baseFactories,
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
backend: {
|
||||
startup: {
|
||||
plugins: {
|
||||
plugin1: {
|
||||
modules: {
|
||||
module1: { onPluginModuleBootFailure: 'continue' },
|
||||
module2: { onPluginModuleBootFailure: 'continue' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
init.add(
|
||||
createBackendPlugin({
|
||||
pluginId: 'plugin1',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
init.add(
|
||||
createBackendModule({
|
||||
pluginId: 'plugin1',
|
||||
moduleId: 'module1',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {
|
||||
throw error1;
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
init.add(
|
||||
createBackendModule({
|
||||
pluginId: 'plugin1',
|
||||
moduleId: 'module2',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {
|
||||
throw error2;
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
init.add(
|
||||
createBackendModule({
|
||||
pluginId: 'plugin1',
|
||||
moduleId: 'module3',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await init.start();
|
||||
|
||||
expect(result.plugins[0].modules).toHaveLength(3);
|
||||
expect(result.plugins[0].modules[0]).toMatchObject({
|
||||
moduleId: 'module1',
|
||||
failure: { error: error1, allowed: true },
|
||||
});
|
||||
expect(result.plugins[0].modules[1]).toMatchObject({
|
||||
moduleId: 'module2',
|
||||
failure: { error: error2, allowed: true },
|
||||
});
|
||||
expect(result.plugins[0].modules[2]).toMatchObject({
|
||||
moduleId: 'module3',
|
||||
});
|
||||
expect(result.plugins[0].modules[2].failure).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return results for plugins without modules', async () => {
|
||||
const init = new BackendInitializer(baseFactories);
|
||||
init.add(
|
||||
createBackendPlugin({
|
||||
pluginId: 'plugin1',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await init.start();
|
||||
|
||||
expect(result.plugins).toHaveLength(1);
|
||||
expect(result.plugins[0]).toMatchObject({
|
||||
pluginId: 'plugin1',
|
||||
modules: [],
|
||||
});
|
||||
expect(result.plugins[0].failure).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should include error information in BackendStartupError', async () => {
|
||||
const error = new Error('Test error');
|
||||
const init = new BackendInitializer(baseFactories);
|
||||
init.add(
|
||||
createBackendPlugin({
|
||||
pluginId: 'plugin1',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {
|
||||
throw error;
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const err = await init.start().then(
|
||||
() => {
|
||||
throw new Error('Expected BackendStartupError to be thrown');
|
||||
},
|
||||
(e: BackendStartupError) => e,
|
||||
);
|
||||
|
||||
expect(err).toBeInstanceOf(BackendStartupError);
|
||||
expect(err?.message).toBe(
|
||||
"Backend startup failed due to the following errors:\n Plugin 'plugin1' startup failed; caused by Error: Test error",
|
||||
);
|
||||
expect(err?.result).toBeDefined();
|
||||
expect(err?.result.plugins[0].failure?.error).toBe(error);
|
||||
});
|
||||
|
||||
it('should handle plugin scoped service factory failures', async () => {
|
||||
const serviceFactoryError = new Error('Service factory failed');
|
||||
const ref = createServiceRef<{ value: string }>({
|
||||
id: 'failing-service',
|
||||
});
|
||||
const init = new BackendInitializer([
|
||||
...baseFactories,
|
||||
createServiceFactory({
|
||||
service: ref,
|
||||
deps: {},
|
||||
factory: () => {
|
||||
throw serviceFactoryError;
|
||||
},
|
||||
}),
|
||||
]);
|
||||
init.add(
|
||||
createBackendPlugin({
|
||||
pluginId: 'plugin1',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: { service: ref },
|
||||
async init() {},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const err = await init.start().then(
|
||||
() => {
|
||||
throw new Error('Expected BackendStartupError to be thrown');
|
||||
},
|
||||
(e: BackendStartupError) => e,
|
||||
);
|
||||
|
||||
expect(err).toBeInstanceOf(BackendStartupError);
|
||||
expect(err?.result.plugins).toHaveLength(1);
|
||||
expect(err?.result.plugins[0].pluginId).toBe('plugin1');
|
||||
expect(err?.result.plugins[0].failure).toBeDefined();
|
||||
expect(err?.result.plugins[0].failure?.allowed).toBe(false);
|
||||
expect(err?.result.plugins[0].failure?.error.message).toContain(
|
||||
"Failed to instantiate service 'failing-service'",
|
||||
);
|
||||
expect(err?.result.plugins[0].failure?.error.message).toContain(
|
||||
'Service factory failed',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle plugin scoped service factory failures with allowed boot failure', async () => {
|
||||
const serviceFactoryError = new Error('Service factory failed');
|
||||
const ref = createServiceRef<{ value: string }>({
|
||||
id: 'failing-service',
|
||||
});
|
||||
const init = new BackendInitializer([
|
||||
...baseFactories,
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
backend: {
|
||||
startup: {
|
||||
plugins: { plugin1: { onPluginBootFailure: 'continue' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
createServiceFactory({
|
||||
service: ref,
|
||||
deps: {},
|
||||
factory: () => {
|
||||
throw serviceFactoryError;
|
||||
},
|
||||
}),
|
||||
]);
|
||||
init.add(
|
||||
createBackendPlugin({
|
||||
pluginId: 'plugin1',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: { service: ref },
|
||||
async init() {},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await init.start();
|
||||
|
||||
expect(result.plugins).toHaveLength(1);
|
||||
expect(result.plugins[0].pluginId).toBe('plugin1');
|
||||
expect(result.plugins[0].failure).toBeDefined();
|
||||
expect(result.plugins[0].failure?.allowed).toBe(true);
|
||||
expect(result.plugins[0].failure?.error.message).toContain(
|
||||
"Failed to instantiate service 'failing-service'",
|
||||
);
|
||||
expect(result.plugins[0].failure?.error.message).toContain(
|
||||
'Service factory failed',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
RootLifecycleService,
|
||||
createServiceFactory,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { Config } from '@backstage/config';
|
||||
import { ServiceOrExtensionPoint } from './types';
|
||||
// Direct internal import to avoid duplication
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
@@ -38,9 +37,12 @@ import type { InternalServiceFactory } from '../../../backend-plugin-api/src/ser
|
||||
import { ForwardedError, ConflictError, assertError } from '@backstage/errors';
|
||||
import { DependencyGraph } from '../lib/DependencyGraph';
|
||||
import { ServiceRegistry } from './ServiceRegistry';
|
||||
import { createInitializationLogger } from './createInitializationLogger';
|
||||
import { createInitializationResultCollector } from './createInitializationResultCollector';
|
||||
import { deepFreeze, unwrapFeature } from './helpers';
|
||||
import type { RootInstanceMetadataServicePluginInfo } from '@backstage/backend-plugin-api';
|
||||
import { BackendStartupResult } from './types';
|
||||
import { BackendStartupError } from './BackendStartupError';
|
||||
import { createAllowBootFailurePredicate } from './createAllowBootFailurePredicate';
|
||||
|
||||
export interface BackendRegisterInit {
|
||||
consumes: Set<ServiceOrExtensionPoint>;
|
||||
@@ -148,7 +150,7 @@ function createRootInstanceMetadataServiceFactory(
|
||||
}
|
||||
|
||||
export class BackendInitializer {
|
||||
#startPromise?: Promise<void>;
|
||||
#startPromise?: Promise<BackendStartupResult>;
|
||||
#stopPromise?: Promise<void>;
|
||||
#registrations = new Array<InternalBackendRegistrations>();
|
||||
#extensionPoints = new Map<string, { impl: unknown; pluginId: string }>();
|
||||
@@ -224,7 +226,7 @@ export class BackendInitializer {
|
||||
}
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
async start(): Promise<BackendStartupResult> {
|
||||
if (this.#startPromise) {
|
||||
throw new Error('Backend has already started');
|
||||
}
|
||||
@@ -235,10 +237,10 @@ export class BackendInitializer {
|
||||
instanceRegistry.register(this);
|
||||
|
||||
this.#startPromise = this.#doStart();
|
||||
await this.#startPromise;
|
||||
return await this.#startPromise;
|
||||
}
|
||||
|
||||
async #doStart(): Promise<void> {
|
||||
async #doStart(): Promise<BackendStartupResult> {
|
||||
this.#serviceRegistry.checkForCircularDeps();
|
||||
|
||||
for (const feature of this.#registeredFeatures) {
|
||||
@@ -332,26 +334,26 @@ export class BackendInitializer {
|
||||
}
|
||||
}
|
||||
|
||||
const allPluginIds = [...pluginInits.keys()];
|
||||
|
||||
const initLogger = createInitializationLogger(
|
||||
allPluginIds,
|
||||
await this.#serviceRegistry.get(coreServices.rootLogger, 'root'),
|
||||
);
|
||||
const pluginIds = [...pluginInits.keys()];
|
||||
|
||||
const rootConfig = await this.#serviceRegistry.get(
|
||||
coreServices.rootConfig,
|
||||
'root',
|
||||
);
|
||||
const rootLogger = await this.#serviceRegistry.get(
|
||||
coreServices.rootLogger,
|
||||
'root',
|
||||
);
|
||||
|
||||
const resultCollector = createInitializationResultCollector({
|
||||
pluginIds,
|
||||
logger: rootLogger,
|
||||
allowBootFailurePredicate: createAllowBootFailurePredicate(rootConfig),
|
||||
});
|
||||
|
||||
// All plugins are initialized in parallel
|
||||
const results = await Promise.allSettled(
|
||||
allPluginIds.map(async pluginId => {
|
||||
const isBootFailurePermitted = this.#getPluginBootFailurePredicate(
|
||||
pluginId,
|
||||
rootConfig,
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
pluginIds.map(async pluginId => {
|
||||
try {
|
||||
// Initialize all eager services
|
||||
await this.#serviceRegistry.initializeEagerServicesWithScope(
|
||||
@@ -382,37 +384,21 @@ export class BackendInitializer {
|
||||
}
|
||||
await tree.parallelTopologicalTraversal(
|
||||
async ({ moduleId, moduleInit }) => {
|
||||
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,
|
||||
);
|
||||
});
|
||||
await moduleInit.init.func(moduleDeps);
|
||||
resultCollector.onPluginModuleResult(pluginId, moduleId);
|
||||
} catch (error: unknown) {
|
||||
assertError(error);
|
||||
if (isModuleBootFailurePermitted) {
|
||||
initLogger.onPermittedPluginModuleFailure(
|
||||
pluginId,
|
||||
moduleId,
|
||||
error,
|
||||
);
|
||||
} else {
|
||||
initLogger.onPluginModuleFailed(pluginId, moduleId, error);
|
||||
throw error;
|
||||
}
|
||||
resultCollector.onPluginModuleResult(
|
||||
pluginId,
|
||||
moduleId,
|
||||
error,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -426,46 +412,36 @@ export class BackendInitializer {
|
||||
pluginInit.init.deps,
|
||||
pluginId,
|
||||
);
|
||||
await pluginInit.init.func(pluginDeps).catch(error => {
|
||||
throw new ForwardedError(
|
||||
`Plugin '${pluginId}' startup failed`,
|
||||
error,
|
||||
);
|
||||
});
|
||||
await pluginInit.init.func(pluginDeps);
|
||||
}
|
||||
|
||||
initLogger.onPluginStarted(pluginId);
|
||||
resultCollector.onPluginResult(pluginId);
|
||||
|
||||
// Once the plugin and all modules have been initialized, we can signal that the plugin has stared up successfully
|
||||
const lifecycleService = await this.#getPluginLifecycleImpl(pluginId);
|
||||
await lifecycleService.startup();
|
||||
} catch (error: unknown) {
|
||||
assertError(error);
|
||||
if (isBootFailurePermitted) {
|
||||
initLogger.onPermittedPluginFailure(pluginId, error);
|
||||
} else {
|
||||
initLogger.onPluginFailed(pluginId, error);
|
||||
throw error;
|
||||
}
|
||||
resultCollector.onPluginResult(pluginId, error);
|
||||
}
|
||||
}),
|
||||
);
|
||||
).catch(error => {
|
||||
throw new ForwardedError(
|
||||
'Unexpected uncaught backend startup error',
|
||||
error,
|
||||
);
|
||||
});
|
||||
|
||||
const initErrors = results.flatMap(r =>
|
||||
r.status === 'rejected' ? [r.reason] : [],
|
||||
);
|
||||
if (initErrors.length === 1) {
|
||||
throw initErrors[0];
|
||||
} else if (initErrors.length > 1) {
|
||||
// TODO(Rugvip): Seems like there aren't proper types for AggregateError yet
|
||||
throw new (AggregateError as any)(initErrors, 'Backend startup failed');
|
||||
const startupResult = resultCollector.finalize();
|
||||
if (startupResult.result === 'failure') {
|
||||
throw new BackendStartupError(startupResult);
|
||||
}
|
||||
|
||||
// Once all plugins and modules have been initialized, we can signal that the backend has started up successfully
|
||||
const lifecycleService = await this.#getRootLifecycleImpl();
|
||||
await lifecycleService.startup();
|
||||
|
||||
initLogger.onAllStarted();
|
||||
return startupResult;
|
||||
}
|
||||
|
||||
// It's fine to call .stop() multiple times, which for example can happen with manual stop + process exit
|
||||
@@ -654,38 +630,6 @@ export class BackendInitializer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#getPluginBootFailurePredicate(pluginId: string, config?: Config): boolean {
|
||||
const defaultStartupBootFailureValue =
|
||||
config?.getOptionalString(
|
||||
'backend.startup.default.onPluginBootFailure',
|
||||
) ?? 'abort';
|
||||
|
||||
const pluginStartupBootFailureValue =
|
||||
config?.getOptionalString(
|
||||
`backend.startup.plugins.${pluginId}.onPluginBootFailure`,
|
||||
) ?? defaultStartupBootFailureValue;
|
||||
|
||||
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(
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CustomErrorBase } from '@backstage/errors';
|
||||
import { BackendStartupResult } from './types';
|
||||
|
||||
function formatMessage(startupResult: BackendStartupResult): string {
|
||||
const parts: string[] = [
|
||||
'Backend startup failed due to the following errors:',
|
||||
];
|
||||
const failures: string[] = [];
|
||||
|
||||
for (const plugin of startupResult.plugins) {
|
||||
if (plugin.failure && !plugin.failure.allowed) {
|
||||
failures.push(
|
||||
` Plugin '${plugin.pluginId}' startup failed; caused by ${plugin.failure.error}`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const mod of plugin.modules) {
|
||||
if (mod.failure && !mod.failure.allowed) {
|
||||
failures.push(
|
||||
` Module '${mod.moduleId}' for plugin '${plugin.pluginId}' startup failed; caused by ${mod.failure.error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
parts.push(...failures);
|
||||
}
|
||||
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when backend startup fails.
|
||||
* Includes detailed startup results for all plugins and modules.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class BackendStartupError extends CustomErrorBase {
|
||||
name = 'BackendStartupError' as const;
|
||||
|
||||
/**
|
||||
* The startup results for all plugins and modules.
|
||||
*/
|
||||
readonly #startupResult: BackendStartupResult;
|
||||
|
||||
constructor(startupResult: BackendStartupResult) {
|
||||
super(formatMessage(startupResult));
|
||||
this.#startupResult = startupResult;
|
||||
}
|
||||
|
||||
get result(): BackendStartupResult {
|
||||
return this.#startupResult;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
import { BackendFeature, ServiceFactory } from '@backstage/backend-plugin-api';
|
||||
import { BackendInitializer } from './BackendInitializer';
|
||||
import { unwrapFeature } from './helpers';
|
||||
import { Backend } from './types';
|
||||
import { Backend, BackendStartupResult } from './types';
|
||||
|
||||
export class BackstageBackend implements Backend {
|
||||
#initializer: BackendInitializer;
|
||||
@@ -34,8 +34,8 @@ export class BackstageBackend implements Backend {
|
||||
}
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
await this.#initializer.start();
|
||||
async start(): Promise<BackendStartupResult> {
|
||||
return await this.#initializer.start();
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RootLoggerService } from '@backstage/backend-plugin-api';
|
||||
|
||||
const LOGGER_INTERVAL_MAX = 60_000;
|
||||
|
||||
function joinIds(ids: Iterable<string>): string {
|
||||
return [...ids].map(id => `'${id}'`).join(', ');
|
||||
}
|
||||
|
||||
export function createInitializationLogger(
|
||||
pluginIds: string[],
|
||||
rootLogger?: RootLoggerService,
|
||||
): {
|
||||
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' });
|
||||
const starting = new Set(pluginIds);
|
||||
const started = new Set<string>();
|
||||
|
||||
logger?.info(`Plugin initialization started: ${joinIds(pluginIds)}`);
|
||||
|
||||
const getInitStatus = () => {
|
||||
let status = '';
|
||||
if (started.size > 0) {
|
||||
status = `, newly initialized: ${joinIds(started)}`;
|
||||
started.clear();
|
||||
}
|
||||
if (starting.size > 0) {
|
||||
status += `, still initializing: ${joinIds(starting)}`;
|
||||
}
|
||||
return status;
|
||||
};
|
||||
|
||||
// Periodically log the initialization status with a fibonacci backoff
|
||||
let interval = 1000;
|
||||
let prevInterval = 0;
|
||||
let timeout: NodeJS.Timeout | undefined;
|
||||
const onTimeout = () => {
|
||||
logger?.info(`Plugin initialization in progress${getInitStatus()}`);
|
||||
|
||||
const nextInterval = Math.min(interval + prevInterval, LOGGER_INTERVAL_MAX);
|
||||
prevInterval = interval;
|
||||
interval = nextInterval;
|
||||
|
||||
timeout = setTimeout(onTimeout, nextInterval);
|
||||
};
|
||||
timeout = setTimeout(onTimeout, interval);
|
||||
|
||||
return {
|
||||
onPluginStarted(pluginId: string) {
|
||||
starting.delete(pluginId);
|
||||
started.add(pluginId);
|
||||
},
|
||||
onPluginFailed(pluginId: string, error: Error) {
|
||||
starting.delete(pluginId);
|
||||
const status =
|
||||
starting.size > 0
|
||||
? `, waiting for ${starting.size} other plugins to finish before shutting down the process`
|
||||
: '';
|
||||
logger?.error(
|
||||
`Plugin '${pluginId}' threw an error during startup${status}.`,
|
||||
error,
|
||||
);
|
||||
},
|
||||
onPermittedPluginFailure(pluginId: string, error: Error) {
|
||||
starting.delete(pluginId);
|
||||
logger?.error(
|
||||
`Plugin '${pluginId}' threw an error during startup, but boot failure is permitted for this plugin so startup will continue.`,
|
||||
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()}`);
|
||||
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = undefined;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RootLoggerService } from '@backstage/backend-plugin-api';
|
||||
import { AllowBootFailurePredicate } from './createAllowBootFailurePredicate';
|
||||
import {
|
||||
BackendStartupResult,
|
||||
PluginStartupResult,
|
||||
ModuleStartupResult,
|
||||
} from './types';
|
||||
|
||||
const LOGGER_INTERVAL_MAX = 60_000;
|
||||
|
||||
function joinIds(ids: Iterable<string>): string {
|
||||
return [...ids].map(id => `'${id}'`).join(', ');
|
||||
}
|
||||
|
||||
export function createInitializationResultCollector(options: {
|
||||
pluginIds: string[];
|
||||
logger?: RootLoggerService;
|
||||
allowBootFailurePredicate: AllowBootFailurePredicate;
|
||||
}): {
|
||||
onPluginResult(pluginId: string, error?: Error): void;
|
||||
onPluginModuleResult(pluginId: string, moduleId: string, error?: Error): void;
|
||||
finalize(): BackendStartupResult;
|
||||
} {
|
||||
const logger = options.logger?.child({ type: 'initialization' });
|
||||
const beginAt = new Date();
|
||||
const starting = new Set(options.pluginIds);
|
||||
const started = new Set<string>();
|
||||
|
||||
let hasDisallowedFailures = false;
|
||||
|
||||
const pluginResults: PluginStartupResult[] = [];
|
||||
const moduleResultsByPlugin: Map<string, ModuleStartupResult[]> = new Map(
|
||||
Array.from(starting).map(pluginId => [pluginId, []]),
|
||||
);
|
||||
|
||||
logger?.info(`Plugin initialization started: ${joinIds(starting)}`);
|
||||
|
||||
const getInitStatus = () => {
|
||||
let status = '';
|
||||
if (started.size > 0) {
|
||||
status = `, newly initialized: ${joinIds(started)}`;
|
||||
started.clear();
|
||||
}
|
||||
if (starting.size > 0) {
|
||||
status += `, still initializing: ${joinIds(starting)}`;
|
||||
}
|
||||
return status;
|
||||
};
|
||||
|
||||
// Periodically log the initialization status with a fibonacci backoff
|
||||
let interval = 1000;
|
||||
let prevInterval = 0;
|
||||
let timeout: NodeJS.Timeout | undefined;
|
||||
const onTimeout = () => {
|
||||
logger?.info(`Plugin initialization in progress${getInitStatus()}`);
|
||||
|
||||
const nextInterval = Math.min(interval + prevInterval, LOGGER_INTERVAL_MAX);
|
||||
prevInterval = interval;
|
||||
interval = nextInterval;
|
||||
|
||||
timeout = setTimeout(onTimeout, nextInterval);
|
||||
};
|
||||
timeout = setTimeout(onTimeout, interval);
|
||||
|
||||
return {
|
||||
onPluginResult(pluginId: string, error?: Error) {
|
||||
starting.delete(pluginId);
|
||||
started.add(pluginId);
|
||||
|
||||
const modules = moduleResultsByPlugin.get(pluginId);
|
||||
if (!modules) {
|
||||
throw new Error(
|
||||
`Failed to push plugin result for nonexistent plugin '${pluginId}'`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!error) {
|
||||
pluginResults.push({
|
||||
pluginId,
|
||||
resultAt: new Date(),
|
||||
modules,
|
||||
});
|
||||
} else {
|
||||
const allowed = options.allowBootFailurePredicate(pluginId);
|
||||
pluginResults.push({
|
||||
pluginId,
|
||||
resultAt: new Date(),
|
||||
modules,
|
||||
failure: {
|
||||
error,
|
||||
allowed,
|
||||
},
|
||||
});
|
||||
if (allowed) {
|
||||
logger?.error(
|
||||
`Plugin '${pluginId}' threw an error during startup, but boot failure is permitted for this plugin so startup will continue.`,
|
||||
error,
|
||||
);
|
||||
} else {
|
||||
hasDisallowedFailures = true;
|
||||
const status =
|
||||
starting.size > 0
|
||||
? `, waiting for ${starting.size} other plugins to finish before shutting down the process`
|
||||
: '';
|
||||
logger?.error(
|
||||
`Plugin '${pluginId}' threw an error during startup${status}.`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
onPluginModuleResult(pluginId: string, moduleId: string, error?: Error) {
|
||||
const moduleResults = moduleResultsByPlugin.get(pluginId);
|
||||
if (!moduleResults) {
|
||||
throw new Error(
|
||||
`Failed to push module result for nonexistent plugin '${pluginId}'`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!error) {
|
||||
moduleResults.push({ moduleId, resultAt: new Date() });
|
||||
} else {
|
||||
const allowed = options.allowBootFailurePredicate(pluginId, moduleId);
|
||||
moduleResults.push({
|
||||
moduleId,
|
||||
resultAt: new Date(),
|
||||
failure: { error, allowed },
|
||||
});
|
||||
if (allowed) {
|
||||
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,
|
||||
);
|
||||
} else {
|
||||
hasDisallowedFailures = true;
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
finalize() {
|
||||
logger?.info(`Plugin initialization complete${getInitStatus()}`);
|
||||
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = undefined;
|
||||
}
|
||||
return {
|
||||
beginAt,
|
||||
resultAt: new Date(),
|
||||
result: hasDisallowedFailures ? 'failure' : 'success',
|
||||
plugins: pluginResults,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -14,5 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type { Backend, CreateSpecializedBackendOptions } from './types';
|
||||
export type {
|
||||
Backend,
|
||||
CreateSpecializedBackendOptions,
|
||||
BackendStartupResult,
|
||||
PluginStartupResult,
|
||||
ModuleStartupResult,
|
||||
} from './types';
|
||||
export { createSpecializedBackend } from './createSpecializedBackend';
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
*/
|
||||
export interface Backend {
|
||||
add(feature: BackendFeature | Promise<{ default: BackendFeature }>): void;
|
||||
start(): Promise<void>;
|
||||
start(): Promise<BackendStartupResult>;
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -43,3 +43,88 @@ export interface CreateSpecializedBackendOptions {
|
||||
export type ServiceOrExtensionPoint<T = unknown> =
|
||||
| ExtensionPoint<T>
|
||||
| ServiceRef<T>;
|
||||
|
||||
/**
|
||||
* Result of a module startup attempt.
|
||||
* @public
|
||||
*/
|
||||
export interface ModuleStartupResult {
|
||||
/**
|
||||
* The time the module startup was completed.
|
||||
*/
|
||||
resultAt: Date;
|
||||
|
||||
/**
|
||||
* The ID of the module.
|
||||
*/
|
||||
moduleId: string;
|
||||
|
||||
/**
|
||||
* If the startup failed, this contains information about the failure.
|
||||
*/
|
||||
failure?: {
|
||||
/**
|
||||
* The error that occurred during startup, if any.
|
||||
*/
|
||||
error: Error;
|
||||
/**
|
||||
* Whether the failure was allowed.
|
||||
*/
|
||||
allowed: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a plugin startup attempt.
|
||||
* @public
|
||||
*/
|
||||
export interface PluginStartupResult {
|
||||
/**
|
||||
* The time the plugin startup was completed.
|
||||
*/
|
||||
resultAt: Date;
|
||||
/**
|
||||
* If the startup failed, this contains information about the failure.
|
||||
*/
|
||||
failure?: {
|
||||
/**
|
||||
* The error that occurred during startup, if any.
|
||||
*/
|
||||
error: Error;
|
||||
/**
|
||||
* Whether the failure was allowed.
|
||||
*/
|
||||
allowed: boolean;
|
||||
};
|
||||
/**
|
||||
* The ID of the plugin.
|
||||
*/
|
||||
pluginId: string;
|
||||
/**
|
||||
* Results for all modules belonging to this plugin.
|
||||
*/
|
||||
modules: ModuleStartupResult[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a backend startup attempt.
|
||||
* @public
|
||||
*/
|
||||
export interface BackendStartupResult {
|
||||
/**
|
||||
* The time the backend startup started.
|
||||
*/
|
||||
beginAt: Date;
|
||||
/**
|
||||
* The time the backend startup was completed.
|
||||
*/
|
||||
resultAt: Date;
|
||||
/**
|
||||
* Results for all plugins that were attempted to start.
|
||||
*/
|
||||
plugins: PluginStartupResult[];
|
||||
/**
|
||||
* The result of the backend startup.
|
||||
*/
|
||||
result: 'success' | 'failure';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user