backend-plugin-api: add new root lifecycle service + impl
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-plugin-api': patch
|
||||
---
|
||||
|
||||
Added `RootLifecycleService` and `rootLifecycleServiceRef`, as well as added a `labels` option to the existing `LifecycleServiceShutdownHook`.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/backend-test-utils': patch
|
||||
'@backstage/backend-defaults': patch
|
||||
---
|
||||
|
||||
Include implementations for the new `rootLifecycleServiceRef`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-app-api': patch
|
||||
---
|
||||
|
||||
Updated implementations for the new `RootLifecycleService`.
|
||||
@@ -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 },
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
+3
-3
@@ -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>();
|
||||
|
||||
Reference in New Issue
Block a user