From 3232e31a23fcc90553dfa02aa7f5ff02bba4a1df Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 26 Oct 2022 14:30:10 +0200 Subject: [PATCH 1/9] backend: Add support for lifecycle events Signed-off-by: Johan Haals --- packages/backend-app-api/api-report.md | 6 ++ .../src/services/implementations/index.ts | 1 + .../implementations/lifecycleService.ts | 81 +++++++++++++++++++ .../backend-defaults/src/CreateBackend.ts | 2 + packages/backend-plugin-api/api-report.md | 13 +++ .../src/services/definitions/index.ts | 5 ++ .../definitions/lifecycleServiceRef.ts | 42 ++++++++++ .../src/service/CatalogPlugin.ts | 9 ++- 8 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 packages/backend-app-api/src/services/implementations/lifecycleService.ts create mode 100644 packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index 4d9fbea4a5..5d200be93c 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -4,6 +4,7 @@ ```ts import { BackendFeature } from '@backstage/backend-plugin-api'; +import { BackendLifecycle } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { HttpRouterService } from '@backstage/backend-plugin-api'; @@ -66,6 +67,11 @@ export type HttpRouterFactoryOptions = { indexPlugin?: string; }; +// @public (undocumented) +export const lifecycleFactory: ( + options?: undefined, +) => ServiceFactory; + // @public (undocumented) export const loggerFactory: (options?: undefined) => ServiceFactory; diff --git a/packages/backend-app-api/src/services/implementations/index.ts b/packages/backend-app-api/src/services/implementations/index.ts index 608399bad9..2c26097f48 100644 --- a/packages/backend-app-api/src/services/implementations/index.ts +++ b/packages/backend-app-api/src/services/implementations/index.ts @@ -25,4 +25,5 @@ export { schedulerFactory } from './schedulerService'; export { tokenManagerFactory } from './tokenManagerService'; export { urlReaderFactory } from './urlReaderService'; export { httpRouterFactory } from './httpRouterService'; +export { lifecycleFactory } from './lifecycleService'; export type { HttpRouterFactoryOptions } from './httpRouterService'; diff --git a/packages/backend-app-api/src/services/implementations/lifecycleService.ts b/packages/backend-app-api/src/services/implementations/lifecycleService.ts new file mode 100644 index 0000000000..3549ec31f2 --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/lifecycleService.ts @@ -0,0 +1,81 @@ +/* + * 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 { + BackendLifecycle, + createServiceFactory, + lifecycleServiceRef, + loggerToWinstonLogger, + pluginMetadataServiceRef, + rootLoggerServiceRef, + ShutdownHookOptions, +} from '@backstage/backend-plugin-api'; +import { Logger } from 'winston'; + +class BackendLifecycleImpl { + constructor(private readonly logger: Logger) {} + #shutdownTasks: Array = []; + + addShutdownHook(options: ShutdownHookOptions & { pluginId: string }): void { + this.#shutdownTasks.push(options); + } + + async shutdown(): Promise { + await Promise.all( + this.#shutdownTasks.map(hook => + hook + .fn() + .catch(e => { + this.logger.error( + `Shutdown hook registered by plugin '${hook.pluginId}' failed with: ${e}`, + ); + }) + .then(() => + this.logger.debug( + `Successfully ran hook registered by plugin ${hook.pluginId}`, + ), + ), + ), + ); + } +} + +class PluginScopedLifecycleImpl implements BackendLifecycle { + constructor( + private readonly lifecycle: BackendLifecycleImpl, + private readonly pluginId: string, + ) {} + addShutdownHook(options: ShutdownHookOptions): void { + this.lifecycle.addShutdownHook({ ...options, pluginId: this.pluginId }); + } +} + +/** @public */ +export const lifecycleFactory = createServiceFactory({ + service: lifecycleServiceRef, + deps: { + logger: rootLoggerServiceRef, + plugin: pluginMetadataServiceRef, + }, + async factory({ logger }) { + const rootLifecycle = new BackendLifecycleImpl( + loggerToWinstonLogger(logger), + ); + process.on('SIGTERM', async () => await rootLifecycle.shutdown()); + return async ({ plugin }) => { + return new PluginScopedLifecycleImpl(rootLifecycle, plugin.getId()); + }; + }, +}); diff --git a/packages/backend-defaults/src/CreateBackend.ts b/packages/backend-defaults/src/CreateBackend.ts index b95bd6f35b..9fde31e3bd 100644 --- a/packages/backend-defaults/src/CreateBackend.ts +++ b/packages/backend-defaults/src/CreateBackend.ts @@ -22,6 +22,7 @@ import { databaseFactory, discoveryFactory, httpRouterFactory, + lifecycleFactory, loggerFactory, permissionsFactory, rootLoggerFactory, @@ -43,6 +44,7 @@ export const defaultServiceFactories = [ tokenManagerFactory, urlReaderFactory, httpRouterFactory, + lifecycleFactory, ]; /** diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index 70ab168053..0183707735 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -24,6 +24,11 @@ export interface BackendFeature { register(reg: BackendRegistrationPoints): void; } +// @public (undocumented) +export interface BackendLifecycle { + addShutdownHook(options: ShutdownHookOptions): void; +} + // @public (undocumented) export interface BackendModuleConfig { // (undocumented) @@ -158,6 +163,9 @@ export interface HttpRouterService { // @public (undocumented) export const httpRouterServiceRef: ServiceRef; +// @public (undocumented) +export const lifecycleServiceRef: ServiceRef; + // @public (undocumented) export interface Logger { // (undocumented) @@ -235,6 +243,11 @@ export type ServiceRef< $$ref: 'service'; }; +// @public (undocumented) +export type ShutdownHookOptions = { + fn: () => Promise; +}; + // @public (undocumented) export const tokenManagerServiceRef: ServiceRef; diff --git a/packages/backend-plugin-api/src/services/definitions/index.ts b/packages/backend-plugin-api/src/services/definitions/index.ts index e5f032ef60..19f75ffeb5 100644 --- a/packages/backend-plugin-api/src/services/definitions/index.ts +++ b/packages/backend-plugin-api/src/services/definitions/index.ts @@ -28,4 +28,9 @@ export { permissionsServiceRef } from './permissionsServiceRef'; export { schedulerServiceRef } from './schedulerServiceRef'; export { rootLoggerServiceRef } from './rootLoggerServiceRef'; export { pluginMetadataServiceRef } from './pluginMetadataServiceRef'; +export { lifecycleServiceRef } from './lifecycleServiceRef'; +export type { + BackendLifecycle, + ShutdownHookOptions, +} from './lifecycleServiceRef'; export type { PluginMetadata } from './pluginMetadataServiceRef'; diff --git a/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts b/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts new file mode 100644 index 0000000000..ffdd4c414b --- /dev/null +++ b/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts @@ -0,0 +1,42 @@ +/* + * 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'; + +/** + * @public + **/ +export type ShutdownHookOptions = { + fn: () => Promise; +}; + +/** + * @public + **/ +export interface BackendLifecycle { + /** + * Register a function to be called and when the backend is shutting down. + */ + addShutdownHook(options: ShutdownHookOptions): void; +} + +/** + * @public + */ +export const lifecycleServiceRef = createServiceRef({ + id: 'core.lifecycle', + scope: 'plugin', +}); diff --git a/plugins/catalog-backend/src/service/CatalogPlugin.ts b/plugins/catalog-backend/src/service/CatalogPlugin.ts index 64e1e52494..84cad86d9a 100644 --- a/plugins/catalog-backend/src/service/CatalogPlugin.ts +++ b/plugins/catalog-backend/src/service/CatalogPlugin.ts @@ -22,6 +22,7 @@ import { permissionsServiceRef, urlReaderServiceRef, httpRouterServiceRef, + lifecycleServiceRef, } from '@backstage/backend-plugin-api'; import { CatalogBuilder } from './CatalogBuilder'; import { @@ -78,6 +79,7 @@ export const catalogPlugin = createBackendPlugin({ permissions: permissionsServiceRef, database: databaseServiceRef, httpRouter: httpRouterServiceRef, + lifecycle: lifecycleServiceRef, }, async init({ logger, @@ -86,6 +88,7 @@ export const catalogPlugin = createBackendPlugin({ database, permissions, httpRouter, + lifecycle, }) { const winstonLogger = loggerToWinstonLogger(logger); const builder = await CatalogBuilder.create({ @@ -100,7 +103,11 @@ export const catalogPlugin = createBackendPlugin({ const { processingEngine, router } = await builder.build(); await processingEngine.start(); - + lifecycle.addShutdownHook({ + fn: async () => { + await processingEngine.stop(); + }, + }); httpRouter.use(router); }, }); From 7cdc99a002bf257876f1b2b6ab014872d1c16dd8 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 9 Nov 2022 15:33:16 +0100 Subject: [PATCH 2/9] Update packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Johan Haals Signed-off-by: Johan Haals --- .../src/services/definitions/lifecycleServiceRef.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts b/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts index ffdd4c414b..3e8dd31fd1 100644 --- a/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts +++ b/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts @@ -28,7 +28,7 @@ export type ShutdownHookOptions = { **/ export interface BackendLifecycle { /** - * Register a function to be called and when the backend is shutting down. + * Register a function to be called when the backend is shutting down. */ addShutdownHook(options: ShutdownHookOptions): void; } From 678a1583039c01b5afe3d9c2b8bea32975c637f1 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 9 Nov 2022 16:02:10 +0100 Subject: [PATCH 3/9] chore: Rename options, add additional logging Signed-off-by: Johan Haals --- .../src/services/implementations/lifecycleService.ts | 12 ++++++++---- .../src/services/definitions/index.ts | 2 +- .../src/services/definitions/lifecycleServiceRef.ts | 4 ++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/backend-app-api/src/services/implementations/lifecycleService.ts b/packages/backend-app-api/src/services/implementations/lifecycleService.ts index 3549ec31f2..578bd9ca01 100644 --- a/packages/backend-app-api/src/services/implementations/lifecycleService.ts +++ b/packages/backend-app-api/src/services/implementations/lifecycleService.ts @@ -20,19 +20,23 @@ import { loggerToWinstonLogger, pluginMetadataServiceRef, rootLoggerServiceRef, - ShutdownHookOptions, + BackendLifecycleShutdownHook, } from '@backstage/backend-plugin-api'; import { Logger } from 'winston'; class BackendLifecycleImpl { constructor(private readonly logger: Logger) {} - #shutdownTasks: Array = []; + #shutdownTasks: Array = + []; - addShutdownHook(options: ShutdownHookOptions & { pluginId: string }): void { + addShutdownHook( + options: BackendLifecycleShutdownHook & { pluginId: string }, + ): void { this.#shutdownTasks.push(options); } async shutdown(): Promise { + this.logger.info(`Running ${this.#shutdownTasks.length} shutdown tasks...`); await Promise.all( this.#shutdownTasks.map(hook => hook @@ -57,7 +61,7 @@ class PluginScopedLifecycleImpl implements BackendLifecycle { private readonly lifecycle: BackendLifecycleImpl, private readonly pluginId: string, ) {} - addShutdownHook(options: ShutdownHookOptions): void { + addShutdownHook(options: BackendLifecycleShutdownHook): void { this.lifecycle.addShutdownHook({ ...options, pluginId: this.pluginId }); } } diff --git a/packages/backend-plugin-api/src/services/definitions/index.ts b/packages/backend-plugin-api/src/services/definitions/index.ts index 19f75ffeb5..cf997a344d 100644 --- a/packages/backend-plugin-api/src/services/definitions/index.ts +++ b/packages/backend-plugin-api/src/services/definitions/index.ts @@ -31,6 +31,6 @@ export { pluginMetadataServiceRef } from './pluginMetadataServiceRef'; export { lifecycleServiceRef } from './lifecycleServiceRef'; export type { BackendLifecycle, - ShutdownHookOptions, + BackendLifecycleShutdownHook, } from './lifecycleServiceRef'; export type { PluginMetadata } from './pluginMetadataServiceRef'; diff --git a/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts b/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts index 3e8dd31fd1..4fd9f8daae 100644 --- a/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts +++ b/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts @@ -19,7 +19,7 @@ import { createServiceRef } from '../system/types'; /** * @public **/ -export type ShutdownHookOptions = { +export type BackendLifecycleShutdownHook = { fn: () => Promise; }; @@ -30,7 +30,7 @@ export interface BackendLifecycle { /** * Register a function to be called when the backend is shutting down. */ - addShutdownHook(options: ShutdownHookOptions): void; + addShutdownHook(options: BackendLifecycleShutdownHook): void; } /** From d0c09089a85eaa5fd33874550a8ed8af4a8c0996 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Thu, 10 Nov 2022 15:16:30 +0100 Subject: [PATCH 4/9] Add support for non async hooks Signed-off-by: Johan Haals --- .../src/services/implementations/lifecycleService.ts | 4 ++-- .../src/services/definitions/lifecycleServiceRef.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend-app-api/src/services/implementations/lifecycleService.ts b/packages/backend-app-api/src/services/implementations/lifecycleService.ts index 578bd9ca01..81f1b16b53 100644 --- a/packages/backend-app-api/src/services/implementations/lifecycleService.ts +++ b/packages/backend-app-api/src/services/implementations/lifecycleService.ts @@ -39,8 +39,8 @@ class BackendLifecycleImpl { this.logger.info(`Running ${this.#shutdownTasks.length} shutdown tasks...`); await Promise.all( this.#shutdownTasks.map(hook => - hook - .fn() + Promise.resolve() + .then(() => hook.fn()) .catch(e => { this.logger.error( `Shutdown hook registered by plugin '${hook.pluginId}' failed with: ${e}`, diff --git a/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts b/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts index 4fd9f8daae..15610e2643 100644 --- a/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts +++ b/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts @@ -20,7 +20,7 @@ import { createServiceRef } from '../system/types'; * @public **/ export type BackendLifecycleShutdownHook = { - fn: () => Promise; + fn: () => void | Promise; }; /** From 263283c7bd2e5f7197a6b6b53c25b0af99e560ee Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Thu, 10 Nov 2022 15:31:42 +0100 Subject: [PATCH 5/9] update api docs Signed-off-by: Johan Haals --- packages/backend-plugin-api/api-report.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index 0183707735..b73a85c32e 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -26,9 +26,14 @@ export interface BackendFeature { // @public (undocumented) export interface BackendLifecycle { - addShutdownHook(options: ShutdownHookOptions): void; + addShutdownHook(options: BackendLifecycleShutdownHook): void; } +// @public (undocumented) +export type BackendLifecycleShutdownHook = { + fn: () => void | Promise; +}; + // @public (undocumented) export interface BackendModuleConfig { // (undocumented) @@ -243,11 +248,6 @@ export type ServiceRef< $$ref: 'service'; }; -// @public (undocumented) -export type ShutdownHookOptions = { - fn: () => Promise; -}; - // @public (undocumented) export const tokenManagerServiceRef: ServiceRef; From f3ac47b6c61dc0e2517dcf9b6b7ec26eaf335da6 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Thu, 10 Nov 2022 16:27:07 +0100 Subject: [PATCH 6/9] Handle multiple signals, prevent concurrent calls Signed-off-by: Johan Haals --- .../implementations/lifecycleService.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/backend-app-api/src/services/implementations/lifecycleService.ts b/packages/backend-app-api/src/services/implementations/lifecycleService.ts index 81f1b16b53..0ee0946469 100644 --- a/packages/backend-app-api/src/services/implementations/lifecycleService.ts +++ b/packages/backend-app-api/src/services/implementations/lifecycleService.ts @@ -24,8 +24,13 @@ import { } from '@backstage/backend-plugin-api'; import { Logger } from 'winston'; +const CALLBACKS = ['SIGTERM', 'SIGINT', 'beforeExit']; class BackendLifecycleImpl { - constructor(private readonly logger: Logger) {} + constructor(private readonly logger: Logger) { + CALLBACKS.map(signal => process.on(signal, () => this.shutdown())); + } + + #isCalled = false; #shutdownTasks: Array = []; @@ -36,6 +41,11 @@ class BackendLifecycleImpl { } async shutdown(): Promise { + if (this.#isCalled) { + return; + } + this.#isCalled = true; + this.logger.info(`Running ${this.#shutdownTasks.length} shutdown tasks...`); await Promise.all( this.#shutdownTasks.map(hook => @@ -47,8 +57,8 @@ class BackendLifecycleImpl { ); }) .then(() => - this.logger.debug( - `Successfully ran hook registered by plugin ${hook.pluginId}`, + this.logger.info( + `Successfully ran shutdown hook registered by plugin ${hook.pluginId}`, ), ), ), @@ -77,7 +87,6 @@ export const lifecycleFactory = createServiceFactory({ const rootLifecycle = new BackendLifecycleImpl( loggerToWinstonLogger(logger), ); - process.on('SIGTERM', async () => await rootLifecycle.shutdown()); return async ({ plugin }) => { return new PluginScopedLifecycleImpl(rootLifecycle, plugin.getId()); }; From e982f77fe3f0f651c9007b938c66b7d3710238e9 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 14 Nov 2022 13:46:21 +0100 Subject: [PATCH 7/9] add changeset Signed-off-by: Johan Haals --- .changeset/silly-wolves-remember.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/silly-wolves-remember.md diff --git a/.changeset/silly-wolves-remember.md b/.changeset/silly-wolves-remember.md new file mode 100644 index 0000000000..f39812db88 --- /dev/null +++ b/.changeset/silly-wolves-remember.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Registered shutdown hook in experimental catalog plugin. From d6dbf1792bfa0bf5fc5f0ddaf3ab15599f01f544 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 14 Nov 2022 13:54:13 +0100 Subject: [PATCH 8/9] add changesets Signed-off-by: Johan Haals --- .changeset/old-keys-leave.md | 5 +++++ .changeset/twenty-dodos-wash.md | 5 +++++ .changeset/young-turkeys-relax.md | 5 +++++ 3 files changed, 15 insertions(+) create mode 100644 .changeset/old-keys-leave.md create mode 100644 .changeset/twenty-dodos-wash.md create mode 100644 .changeset/young-turkeys-relax.md diff --git a/.changeset/old-keys-leave.md b/.changeset/old-keys-leave.md new file mode 100644 index 0000000000..d80574b2e4 --- /dev/null +++ b/.changeset/old-keys-leave.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Added `lifecycleFactory` implementation. diff --git a/.changeset/twenty-dodos-wash.md b/.changeset/twenty-dodos-wash.md new file mode 100644 index 0000000000..d21088a953 --- /dev/null +++ b/.changeset/twenty-dodos-wash.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Added `lifecycleFactory` to default service factories. diff --git a/.changeset/young-turkeys-relax.md b/.changeset/young-turkeys-relax.md new file mode 100644 index 0000000000..c53cd984a6 --- /dev/null +++ b/.changeset/young-turkeys-relax.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Added initial support for registering shutdown hooks via `lifecycleServiceRef`. From c94d42b58b737dbcba19aaeb21940ba8ee6efe1f Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 16 Nov 2022 09:09:12 +0100 Subject: [PATCH 9/9] document, add tests Signed-off-by: Johan Haals --- packages/backend-app-api/api-report.md | 2 +- .../implementations/lifecycleService.test.ts | 47 +++++++++++++++++++ .../implementations/lifecycleService.ts | 6 ++- 3 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 packages/backend-app-api/src/services/implementations/lifecycleService.test.ts diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index 5d200be93c..5008b1efe1 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -67,7 +67,7 @@ export type HttpRouterFactoryOptions = { indexPlugin?: string; }; -// @public (undocumented) +// @public export const lifecycleFactory: ( options?: undefined, ) => ServiceFactory; diff --git a/packages/backend-app-api/src/services/implementations/lifecycleService.test.ts b/packages/backend-app-api/src/services/implementations/lifecycleService.test.ts new file mode 100644 index 0000000000..c0b69a3bef --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/lifecycleService.test.ts @@ -0,0 +1,47 @@ +/* + * 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 { getVoidLogger } from '@backstage/backend-common'; +import { BackendLifecycleImpl } from './lifecycleService'; + +describe('lifecycleService', () => { + it('should execute registered shutdown hook', async () => { + const service = new BackendLifecycleImpl(getVoidLogger()); + const hook = jest.fn(); + service.addShutdownHook({ + pluginId: 'test', + fn: async () => { + hook(); + }, + }); + // should not execute the hook more than once. + await service.shutdown(); + await service.shutdown(); + await service.shutdown(); + expect(hook).toHaveBeenCalledTimes(1); + }); + + it('should not throw errors', async () => { + const service = new BackendLifecycleImpl(getVoidLogger()); + service.addShutdownHook({ + pluginId: 'test', + fn: async () => { + throw new Error('oh no'); + }, + }); + await expect(service.shutdown()).resolves.toBeUndefined(); + }); +}); diff --git a/packages/backend-app-api/src/services/implementations/lifecycleService.ts b/packages/backend-app-api/src/services/implementations/lifecycleService.ts index 0ee0946469..7d41a7a276 100644 --- a/packages/backend-app-api/src/services/implementations/lifecycleService.ts +++ b/packages/backend-app-api/src/services/implementations/lifecycleService.ts @@ -25,7 +25,7 @@ import { import { Logger } from 'winston'; const CALLBACKS = ['SIGTERM', 'SIGINT', 'beforeExit']; -class BackendLifecycleImpl { +export class BackendLifecycleImpl { constructor(private readonly logger: Logger) { CALLBACKS.map(signal => process.on(signal, () => this.shutdown())); } @@ -76,7 +76,9 @@ class PluginScopedLifecycleImpl implements BackendLifecycle { } } -/** @public */ +/** + * Allows plugins to register shutdown hooks that are run when the process is about to exit. + * @public */ export const lifecycleFactory = createServiceFactory({ service: lifecycleServiceRef, deps: {