diff --git a/.changeset/beige-rats-cheer.md b/.changeset/beige-rats-cheer.md new file mode 100644 index 0000000000..c523ffa104 --- /dev/null +++ b/.changeset/beige-rats-cheer.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Added `RootLifecycleService` and `rootLifecycleServiceRef`, as well as added a `labels` option to the existing `LifecycleServiceShutdownHook`. diff --git a/.changeset/clever-years-hang.md b/.changeset/clever-years-hang.md new file mode 100644 index 0000000000..31db564c52 --- /dev/null +++ b/.changeset/clever-years-hang.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-test-utils': patch +'@backstage/backend-defaults': patch +--- + +Include implementations for the new `rootLifecycleServiceRef`. diff --git a/.changeset/twenty-nails-camp.md b/.changeset/twenty-nails-camp.md new file mode 100644 index 0000000000..3a8e2e4c3f --- /dev/null +++ b/.changeset/twenty-nails-camp.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Updated implementations for the new `RootLifecycleService`. diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index 33e732cf05..ef88d9a9d0 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -83,6 +83,11 @@ export const permissionsFactory: ( options?: undefined, ) => ServiceFactory; +// @public +export const rootLifecycleFactory: ( + options?: undefined, +) => ServiceFactory; + // @public (undocumented) export const rootLoggerFactory: ( options?: undefined, diff --git a/packages/backend-app-api/src/services/implementations/index.ts b/packages/backend-app-api/src/services/implementations/index.ts index 2c26097f48..c87011d570 100644 --- a/packages/backend-app-api/src/services/implementations/index.ts +++ b/packages/backend-app-api/src/services/implementations/index.ts @@ -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'; diff --git a/packages/backend-app-api/src/services/implementations/lifecycleService.ts b/packages/backend-app-api/src/services/implementations/lifecycleService.ts index 317a2e45b8..bb864679b8 100644 --- a/packages/backend-app-api/src/services/implementations/lifecycleService.ts +++ b/packages/backend-app-api/src/services/implementations/lifecycleService.ts @@ -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 = - []; - - addShutdownHook( - options: LifecycleServiceShutdownHook & { pluginId: string }, - ): void { - this.#shutdownTasks.push(options); - } - - 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 => - 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 }, + }); + }, + }; }; }, }); diff --git a/packages/backend-app-api/src/services/implementations/lifecycleService.test.ts b/packages/backend-app-api/src/services/implementations/rootLifecycleService.test.ts similarity index 91% rename from packages/backend-app-api/src/services/implementations/lifecycleService.test.ts rename to packages/backend-app-api/src/services/implementations/rootLifecycleService.test.ts index c0b69a3bef..686827fecd 100644 --- a/packages/backend-app-api/src/services/implementations/lifecycleService.test.ts +++ b/packages/backend-app-api/src/services/implementations/rootLifecycleService.test.ts @@ -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'); }, diff --git a/packages/backend-app-api/src/services/implementations/rootLifecycleService.ts b/packages/backend-app-api/src/services/implementations/rootLifecycleService.ts new file mode 100644 index 0000000000..c4ad4678ba --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/rootLifecycleService.ts @@ -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 = []; + + addShutdownHook(options: LifecycleServiceShutdownHook): void { + this.#shutdownTasks.push(options); + } + + 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(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)); + }, +}); diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index 478cb30ff8..69512ba4c2 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -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'); } diff --git a/packages/backend-defaults/src/CreateBackend.ts b/packages/backend-defaults/src/CreateBackend.ts index 9fde31e3bd..a37a8c32b2 100644 --- a/packages/backend-defaults/src/CreateBackend.ts +++ b/packages/backend-defaults/src/CreateBackend.ts @@ -23,6 +23,7 @@ import { discoveryFactory, httpRouterFactory, lifecycleFactory, + rootLifecycleFactory, loggerFactory, permissionsFactory, rootLoggerFactory, @@ -45,6 +46,7 @@ export const defaultServiceFactories = [ urlReaderFactory, httpRouterFactory, lifecycleFactory, + rootLifecycleFactory, ]; /** diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index 545728b386..6ecedd0068 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -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; // @public (undocumented) export type LifecycleServiceShutdownHook = { fn: () => void | Promise; + labels?: Record; }; // @public (undocumented) @@ -245,6 +247,12 @@ export interface PluginMetadataService { // @public (undocumented) const pluginMetadataServiceRef: ServiceRef; +// @public (undocumented) +export type RootLifecycleService = LifecycleService; + +// @public (undocumented) +const rootLifecycleServiceRef: ServiceRef; + // @public (undocumented) export type RootLoggerService = LoggerService; diff --git a/packages/backend-plugin-api/src/services/definitions/coreServices.ts b/packages/backend-plugin-api/src/services/definitions/coreServices.ts index d5a5953806..a2b1a04d41 100644 --- a/packages/backend-plugin-api/src/services/definitions/coreServices.ts +++ b/packages/backend-plugin-api/src/services/definitions/coreServices.ts @@ -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'; diff --git a/packages/backend-plugin-api/src/services/definitions/index.ts b/packages/backend-plugin-api/src/services/definitions/index.ts index 3cf2ab4c77..aa01adf446 100644 --- a/packages/backend-plugin-api/src/services/definitions/index.ts +++ b/packages/backend-plugin-api/src/services/definitions/index.ts @@ -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'; diff --git a/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts b/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts index 4061cc1da2..e54f650fcc 100644 --- a/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts +++ b/packages/backend-plugin-api/src/services/definitions/lifecycleServiceRef.ts @@ -21,6 +21,9 @@ import { createServiceRef } from '../system/types'; **/ export type LifecycleServiceShutdownHook = { fn: () => void | Promise; + + /** Labels to help identify the shutdown hook */ + labels?: Record; }; /** diff --git a/packages/backend-plugin-api/src/services/definitions/rootLifecycleServiceRef.ts b/packages/backend-plugin-api/src/services/definitions/rootLifecycleServiceRef.ts new file mode 100644 index 0000000000..e0d5355983 --- /dev/null +++ b/packages/backend-plugin-api/src/services/definitions/rootLifecycleServiceRef.ts @@ -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({ + id: 'core.rootLifecycle', + scope: 'root', +}); diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.ts index 1d401bbf6f..647ebc9dda 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.ts @@ -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();