Ensure root lifecycle shutdown hooks happen after plugins'

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Eric Peterson
2024-10-01 12:57:43 +02:00
parent b1254b40a8
commit 8bfe9f0f61
2 changed files with 63 additions and 12 deletions
@@ -407,6 +407,26 @@ export class BackendInitializer {
// The startup failed, but we may still want to do cleanup so we continue silently
}
// Get all plugins.
const pluginMap = new Map<string, boolean>();
for (const feature of this.#registrations) {
for (const r of feature.getRegistrations()) {
if (r.type === 'plugin') {
pluginMap.set(r.pluginId, true);
}
}
}
const allPluginIds = Array.from(pluginMap.keys());
// Iterate through all plugins and run their shutdown hooks.
await Promise.allSettled(
allPluginIds.map(async pluginId => {
const lifecycleService = await this.#getPluginLifecycleImpl(pluginId);
await lifecycleService.shutdown();
}),
);
// Once all plugin shutdown hooks are done, run root shutdown hooks.
const lifecycleService = await this.#getRootLifecycleImpl();
await lifecycleService.shutdown();
}
@@ -437,7 +457,9 @@ export class BackendInitializer {
async #getPluginLifecycleImpl(
pluginId: string,
): Promise<LifecycleService & { startup(): Promise<void> }> {
): Promise<
LifecycleService & { startup(): Promise<void>; shutdown(): Promise<void> }
> {
const lifecycleService = await this.#serviceRegistry.get(
coreServices.lifecycle,
pluginId,
@@ -22,7 +22,6 @@ import {
LifecycleServiceStartupOptions,
LoggerService,
PluginMetadataService,
RootLifecycleService,
coreServices,
createServiceFactory,
} from '@backstage/backend-plugin-api';
@@ -31,15 +30,19 @@ import {
export class BackendPluginLifecycleImpl implements LifecycleService {
constructor(
private readonly logger: LoggerService,
private readonly rootLifecycle: RootLifecycleService,
private readonly pluginMetadata: PluginMetadataService,
) {}
#hasStarted = false;
#hasShutdown = false;
#startupTasks: Array<{
hook: LifecycleServiceStartupHook;
options?: LifecycleServiceStartupOptions;
}> = [];
#shutdownTasks: Array<{
hook: LifecycleServiceShutdownHook;
options?: LifecycleServiceShutdownOptions;
}> = [];
addStartupHook(
hook: LifecycleServiceStartupHook,
@@ -77,11 +80,42 @@ export class BackendPluginLifecycleImpl implements LifecycleService {
hook: LifecycleServiceShutdownHook,
options?: LifecycleServiceShutdownOptions,
): void {
if (this.#hasShutdown) {
throw new Error('Attempted to add shutdown hook after shutdown');
}
const plugin = this.pluginMetadata.getId();
this.rootLifecycle.addShutdownHook(hook, {
logger: options?.logger?.child({ plugin }) ?? this.logger,
const logger = options?.logger?.child({ plugin }) ?? this.logger;
this.#shutdownTasks.push({
hook,
options: {
...options,
logger,
},
});
}
async shutdown(): Promise<void> {
if (this.#hasShutdown) {
return;
}
this.#hasShutdown = true;
this.logger.debug(
`Running ${this.#shutdownTasks.length} plugin shutdown tasks...`,
);
await Promise.all(
this.#shutdownTasks.map(async ({ hook, options }) => {
const logger = options?.logger ?? this.logger;
try {
await hook();
logger.debug(`Plugin shutdown hook succeeded`);
} catch (error) {
logger.error(`Plugin shutdown hook failed, ${error}`);
}
}),
);
}
}
/**
@@ -97,14 +131,9 @@ export const lifecycleServiceFactory = createServiceFactory({
service: coreServices.lifecycle,
deps: {
logger: coreServices.logger,
rootLifecycle: coreServices.rootLifecycle,
pluginMetadata: coreServices.pluginMetadata,
},
async factory({ rootLifecycle, logger, pluginMetadata }) {
return new BackendPluginLifecycleImpl(
logger,
rootLifecycle,
pluginMetadata,
);
async factory({ logger, pluginMetadata }) {
return new BackendPluginLifecycleImpl(logger, pluginMetadata);
},
});