backend-plugin-api: add new root lifecycle service + impl

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-01-02 15:56:50 +01:00
parent 32eb67eb75
commit 6cfd4d7073
16 changed files with 157 additions and 71 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-plugin-api': patch
---
Added `RootLifecycleService` and `rootLifecycleServiceRef`, as well as added a `labels` option to the existing `LifecycleServiceShutdownHook`.
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/backend-test-utils': patch
'@backstage/backend-defaults': patch
---
Include implementations for the new `rootLifecycleServiceRef`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-app-api': patch
---
Updated implementations for the new `RootLifecycleService`.
+5
View File
@@ -83,6 +83,11 @@ export const permissionsFactory: (
options?: undefined,
) => ServiceFactory<PermissionsService>;
// @public
export const rootLifecycleFactory: (
options?: undefined,
) => ServiceFactory<LifecycleService>;
// @public (undocumented)
export const rootLoggerFactory: (
options?: undefined,
@@ -26,4 +26,5 @@ export { tokenManagerFactory } from './tokenManagerService';
export { urlReaderFactory } from './urlReaderService';
export { httpRouterFactory } from './httpRouterService';
export { lifecycleFactory } from './lifecycleService';
export { rootLifecycleFactory } from './rootLifecycleService';
export type { HttpRouterFactoryOptions } from './httpRouterService';
@@ -14,65 +14,10 @@
* limitations under the License.
*/
import {
LifecycleService,
createServiceFactory,
coreServices,
loggerToWinstonLogger,
LifecycleServiceShutdownHook,
} from '@backstage/backend-plugin-api';
import { Logger } from 'winston';
const CALLBACKS = ['SIGTERM', 'SIGINT', 'beforeExit'];
export class BackendLifecycleImpl {
constructor(private readonly logger: Logger) {
CALLBACKS.map(signal => process.on(signal, () => this.shutdown()));
}
#isCalled = false;
#shutdownTasks: Array<LifecycleServiceShutdownHook & { pluginId: string }> =
[];
addShutdownHook(
options: LifecycleServiceShutdownHook & { pluginId: string },
): void {
this.#shutdownTasks.push(options);
}
async shutdown(): Promise<void> {
if (this.#isCalled) {
return;
}
this.#isCalled = true;
this.logger.info(`Running ${this.#shutdownTasks.length} shutdown tasks...`);
await Promise.all(
this.#shutdownTasks.map(hook =>
Promise.resolve()
.then(() => hook.fn())
.catch(e => {
this.logger.error(
`Shutdown hook registered by plugin '${hook.pluginId}' failed with: ${e}`,
);
})
.then(() =>
this.logger.info(
`Successfully ran shutdown hook registered by plugin ${hook.pluginId}`,
),
),
),
);
}
}
class PluginScopedLifecycleImpl implements LifecycleService {
constructor(
private readonly lifecycle: BackendLifecycleImpl,
private readonly pluginId: string,
) {}
addShutdownHook(options: LifecycleServiceShutdownHook): void {
this.lifecycle.addShutdownHook({ ...options, pluginId: this.pluginId });
}
}
/**
* Allows plugins to register shutdown hooks that are run when the process is about to exit.
@@ -80,15 +25,20 @@ class PluginScopedLifecycleImpl implements LifecycleService {
export const lifecycleFactory = createServiceFactory({
service: coreServices.lifecycle,
deps: {
logger: coreServices.rootLogger,
plugin: coreServices.pluginMetadata,
rootLifecycle: coreServices.rootLifecycle,
pluginMetadata: coreServices.pluginMetadata,
},
async factory({ logger }) {
const rootLifecycle = new BackendLifecycleImpl(
loggerToWinstonLogger(logger),
);
return async ({ plugin }) => {
return new PluginScopedLifecycleImpl(rootLifecycle, plugin.getId());
async factory({ rootLifecycle }) {
return async ({ pluginMetadata }) => {
const plugin = pluginMetadata.getId();
return {
addShutdownHook(options: LifecycleServiceShutdownHook): void {
rootLifecycle.addShutdownHook({
...options,
labels: { ...options?.labels, plugin },
});
},
};
};
},
});
@@ -15,14 +15,14 @@
*/
import { getVoidLogger } from '@backstage/backend-common';
import { BackendLifecycleImpl } from './lifecycleService';
import { BackendLifecycleImpl } from './rootLifecycleService';
describe('lifecycleService', () => {
it('should execute registered shutdown hook', async () => {
const service = new BackendLifecycleImpl(getVoidLogger());
const hook = jest.fn();
service.addShutdownHook({
pluginId: 'test',
labels: { plugin: 'test' },
fn: async () => {
hook();
},
@@ -37,7 +37,7 @@ describe('lifecycleService', () => {
it('should not throw errors', async () => {
const service = new BackendLifecycleImpl(getVoidLogger());
service.addShutdownHook({
pluginId: 'test',
labels: { plugin: 'test' },
fn: async () => {
throw new Error('oh no');
},
@@ -0,0 +1,69 @@
/*
* 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 {
createServiceFactory,
coreServices,
loggerToWinstonLogger,
LifecycleServiceShutdownHook,
RootLifecycleService,
} from '@backstage/backend-plugin-api';
import { Logger } from 'winston';
const CALLBACKS = ['SIGTERM', 'SIGINT', 'beforeExit'];
export class BackendLifecycleImpl implements RootLifecycleService {
constructor(private readonly logger: Logger) {
CALLBACKS.map(signal => process.on(signal, () => this.shutdown()));
}
#isCalled = false;
#shutdownTasks: Array<LifecycleServiceShutdownHook> = [];
addShutdownHook(options: LifecycleServiceShutdownHook): void {
this.#shutdownTasks.push(options);
}
async shutdown(): Promise<void> {
if (this.#isCalled) {
return;
}
this.#isCalled = true;
this.logger.info(`Running ${this.#shutdownTasks.length} shutdown tasks...`);
await Promise.all(
this.#shutdownTasks.map(async hook => {
try {
await hook.fn();
this.logger.info(`Shutdown hook succeeded`, hook.labels);
} catch (error) {
this.logger.error(`Shutdown hook failed, ${error}`, hook.labels);
}
}),
);
}
}
/**
* Allows plugins to register shutdown hooks that are run when the process is about to exit.
* @public */
export const rootLifecycleFactory = createServiceFactory({
service: coreServices.rootLifecycle,
deps: {
logger: coreServices.rootLogger,
},
async factory({ logger }) {
return new BackendLifecycleImpl(loggerToWinstonLogger(logger));
},
});
@@ -20,7 +20,7 @@ import {
coreServices,
ServiceRef,
} from '@backstage/backend-plugin-api';
import { BackendLifecycleImpl } from '../services/implementations/lifecycleService';
import { BackendLifecycleImpl } from '../services/implementations/rootLifecycleService';
import {
BackendRegisterInit,
EnumerableServiceHolder,
@@ -182,14 +182,13 @@ export class BackendInitializer {
}
const lifecycleService = await this.#serviceHolder.get(
coreServices.lifecycle,
coreServices.rootLifecycle,
'root',
);
// TODO(Rugvip): Find a better way to do this
const lifecycle = (lifecycleService as any)?.lifecycle;
if (lifecycle instanceof BackendLifecycleImpl) {
await lifecycle.shutdown();
if (lifecycleService instanceof BackendLifecycleImpl) {
await lifecycleService.shutdown();
} else {
throw new Error('Unexpected lifecycle service implementation');
}
@@ -23,6 +23,7 @@ import {
discoveryFactory,
httpRouterFactory,
lifecycleFactory,
rootLifecycleFactory,
loggerFactory,
permissionsFactory,
rootLoggerFactory,
@@ -45,6 +46,7 @@ export const defaultServiceFactories = [
urlReaderFactory,
httpRouterFactory,
lifecycleFactory,
rootLifecycleFactory,
];
/**
@@ -89,6 +89,7 @@ declare namespace coreServices {
tokenManagerServiceRef as tokenManager,
permissionsServiceRef as permissions,
schedulerServiceRef as scheduler,
rootLifecycleServiceRef as rootLifecycle,
rootLoggerServiceRef as rootLogger,
pluginMetadataServiceRef as pluginMetadata,
lifecycleServiceRef as lifecycle,
@@ -200,6 +201,7 @@ const lifecycleServiceRef: ServiceRef<LifecycleService, 'plugin'>;
// @public (undocumented)
export type LifecycleServiceShutdownHook = {
fn: () => void | Promise<void>;
labels?: Record<string, string>;
};
// @public (undocumented)
@@ -245,6 +247,12 @@ export interface PluginMetadataService {
// @public (undocumented)
const pluginMetadataServiceRef: ServiceRef<PluginMetadataService, 'plugin'>;
// @public (undocumented)
export type RootLifecycleService = LifecycleService;
// @public (undocumented)
const rootLifecycleServiceRef: ServiceRef<LifecycleService, 'root'>;
// @public (undocumented)
export type RootLoggerService = LoggerService;
@@ -24,6 +24,7 @@ export { discoveryServiceRef as discovery } from './discoveryServiceRef';
export { tokenManagerServiceRef as tokenManager } from './tokenManagerServiceRef';
export { permissionsServiceRef as permissions } from './permissionsServiceRef';
export { schedulerServiceRef as scheduler } from './schedulerServiceRef';
export { rootLifecycleServiceRef as rootLifecycle } from './rootLifecycleServiceRef';
export { rootLoggerServiceRef as rootLogger } from './rootLoggerServiceRef';
export { pluginMetadataServiceRef as pluginMetadata } from './pluginMetadataServiceRef';
export { lifecycleServiceRef as lifecycle } from './lifecycleServiceRef';
@@ -29,6 +29,7 @@ export type {
export type { LoggerService, LogMeta } from './loggerServiceRef';
export type { PermissionsService } from './permissionsServiceRef';
export type { PluginMetadataService } from './pluginMetadataServiceRef';
export type { RootLifecycleService } from './rootLifecycleServiceRef';
export type { RootLoggerService } from './rootLoggerServiceRef';
export type { SchedulerService } from './schedulerServiceRef';
export type { TokenManagerService } from './tokenManagerServiceRef';
@@ -21,6 +21,9 @@ import { createServiceRef } from '../system/types';
**/
export type LifecycleServiceShutdownHook = {
fn: () => void | Promise<void>;
/** Labels to help identify the shutdown hook */
labels?: Record<string, string>;
};
/**
@@ -0,0 +1,29 @@
/*
* 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 { createServiceRef } from '../system/types';
import { LifecycleService } from './lifecycleServiceRef';
/** @public */
export type RootLifecycleService = LifecycleService;
/**
* @public
*/
export const rootLifecycleServiceRef = createServiceRef<RootLifecycleService>({
id: 'core.rootLifecycle',
scope: 'root',
});
@@ -18,6 +18,7 @@ import {
Backend,
createSpecializedBackend,
lifecycleFactory,
rootLifecycleFactory,
loggerFactory,
rootLoggerFactory,
} from '@backstage/backend-app-api';
@@ -57,6 +58,7 @@ const defaultServiceFactories = [
rootLoggerFactory(),
loggerFactory(),
lifecycleFactory(),
rootLifecycleFactory(),
];
const backendInstancesToCleanUp = new Array<Backend>();