From 154632d8753b16c0343c8c91954b06effed7abe9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 6 Sep 2023 13:30:34 +0200 Subject: [PATCH] backend-app-api: add support for discoverying additional service factories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: Johan Haals Co-authored-by: Philipp Hugenroth Co-authored-by: Camila Belo Signed-off-by: Patrik Oldsberg --- .changeset/chatty-spiders-serve.md | 5 +++ .../src/wiring/BackendInitializer.ts | 44 ++++++------------- .../src/wiring/ServiceRegistry.test.ts | 26 +++++++++++ .../src/wiring/ServiceRegistry.ts | 35 +++++++++++++-- packages/backend-app-api/src/wiring/types.ts | 11 ----- 5 files changed, 75 insertions(+), 46 deletions(-) create mode 100644 .changeset/chatty-spiders-serve.md diff --git a/.changeset/chatty-spiders-serve.md b/.changeset/chatty-spiders-serve.md new file mode 100644 index 0000000000..e2b5e5630e --- /dev/null +++ b/.changeset/chatty-spiders-serve.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Add support for discovering additional service factories during startup. diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index dbc807b9c3..03ca10882c 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -23,7 +23,7 @@ import { } from '@backstage/backend-plugin-api'; import { BackendLifecycleImpl } from '../services/implementations/rootLifecycle/rootLifecycleServiceFactory'; import { BackendPluginLifecycleImpl } from '../services/implementations/lifecycle/lifecycleServiceFactory'; -import { EnumerableServiceHolder, ServiceOrExtensionPoint } from './types'; +import { ServiceOrExtensionPoint } from './types'; // Direct internal import to avoid duplication // eslint-disable-next-line @backstage/no-forbidden-package-imports import { InternalBackendFeature } from '@backstage/backend-plugin-api/src/wiring/types'; @@ -45,12 +45,10 @@ export class BackendInitializer { #startPromise?: Promise; #features = new Array(); #extensionPoints = new Map(); - #serviceHolder: EnumerableServiceHolder | undefined; - #providedServiceFactories = new Array(); - #defaultApiFactories: ServiceFactory[]; + #serviceRegistry: ServiceRegistry; constructor(defaultApiFactories: ServiceFactory[]) { - this.#defaultApiFactories = defaultApiFactories; + this.#serviceRegistry = ServiceRegistry.create([...defaultApiFactories]); } async #getInitDeps( @@ -70,7 +68,7 @@ export class BackendInitializer { } result.set(name, ep.impl); } else { - const impl = await this.#serviceHolder!.get( + const impl = await this.#serviceRegistry.get( ref as ServiceRef, pluginId, ); @@ -107,21 +105,7 @@ export class BackendInitializer { } if (isServiceFactory(feature)) { - if (feature.service.id === coreServices.pluginMetadata.id) { - throw new Error( - `The ${coreServices.pluginMetadata.id} service cannot be overridden`, - ); - } - if ( - this.#providedServiceFactories.find( - sf => sf.service.id === feature.service.id, - ) - ) { - throw new Error( - `Duplicate service implementations provided for ${feature.service.id}`, - ); - } - this.#providedServiceFactories.push(feature); + this.#serviceRegistry.add(feature); } else if (isInternalBackendFeature(feature)) { if (feature.version !== 'v1') { throw new Error( @@ -164,12 +148,9 @@ export class BackendInitializer { } async #doStart(): Promise { - this.#serviceHolder = ServiceRegistry.create([ - ...this.#defaultApiFactories, - ...this.#providedServiceFactories, - ]); + this.#serviceRegistry.checkForCircularDeps(); - const featureDiscovery = await this.#serviceHolder.get( + const featureDiscovery = await this.#serviceRegistry.get( featureDiscoveryServiceRef, 'root', ); @@ -179,12 +160,13 @@ export class BackendInitializer { for (const feature of features) { this.#addFeature(feature); } + this.#serviceRegistry.checkForCircularDeps(); } // Initialize all root scoped services - for (const ref of this.#serviceHolder.getServiceRefs()) { + for (const ref of this.#serviceRegistry.getServiceRefs()) { if (ref.scope === 'root') { - await this.#serviceHolder.get(ref, 'root'); + await this.#serviceRegistry.get(ref, 'root'); } } @@ -313,7 +295,7 @@ export class BackendInitializer { // Once the backend is started, any uncaught errors or unhandled rejections are caught // and logged, in order to avoid crashing the entire backend on local failures. if (process.env.NODE_ENV !== 'test') { - const rootLogger = await this.#serviceHolder.get( + const rootLogger = await this.#serviceRegistry.get( coreServices.rootLogger, 'root', ); @@ -347,7 +329,7 @@ export class BackendInitializer { // Bit of a hacky way to grab the lifecycle services, potentially find a nicer way to do this async #getRootLifecycleImpl(): Promise { - const lifecycleService = await this.#serviceHolder!.get( + const lifecycleService = await this.#serviceRegistry.get( coreServices.rootLifecycle, 'root', ); @@ -360,7 +342,7 @@ export class BackendInitializer { async #getPluginLifecycleImpl( pluginId: string, ): Promise { - const lifecycleService = await this.#serviceHolder!.get( + const lifecycleService = await this.#serviceRegistry.get( coreServices.lifecycle, pluginId, ); diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts index 76bcd071cd..18e2ac4398 100644 --- a/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts +++ b/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts @@ -188,6 +188,32 @@ describe('ServiceRegistry', () => { }); }); + it('should use added service factories for each ref', async () => { + const registry = ServiceRegistry.create([sf2()]); + registry.add(sf2b()); + await expect(registry.get(ref2, 'catalog')).resolves.toEqual({ + x: 22, + }); + }); + + it('should not allow factories to be added after instantiation', async () => { + const registry = ServiceRegistry.create([sf2()]); + await expect(registry.get(ref2, 'catalog')).resolves.toEqual({ + x: 2, + }); + expect(() => registry.add(sf2b())).toThrow( + 'Unable to set service factory with id 2, service has already been instantiated', + ); + }); + + it('should not allow the same factory to be added twice', async () => { + const registry = ServiceRegistry.create([sf2()]); + registry.add(sf2b()); + expect(() => registry.add(sf2b())).toThrow( + 'Duplicate service implementations provided for 2', + ); + }); + it('should use the defaultFactory from the ref if not provided to the registry', async () => { const registry = ServiceRegistry.create([]); await expect(registry.get(refDefault1, 'catalog')).resolves.toEqual({ diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.ts index d009a319ba..ad52e645bb 100644 --- a/packages/backend-app-api/src/wiring/ServiceRegistry.ts +++ b/packages/backend-app-api/src/wiring/ServiceRegistry.ts @@ -21,7 +21,6 @@ import { createServiceFactory, } from '@backstage/backend-plugin-api'; import { ConflictError, stringifyError } from '@backstage/errors'; -import { EnumerableServiceHolder } from './types'; // Direct internal import to avoid duplication // eslint-disable-next-line @backstage/no-forbidden-package-imports import { InternalServiceFactory } from '@backstage/backend-plugin-api/src/services/system/types'; @@ -57,10 +56,10 @@ const pluginMetadataServiceFactory = createServiceFactory( }), ); -export class ServiceRegistry implements EnumerableServiceHolder { +export class ServiceRegistry { static create(factories: Array): ServiceRegistry { const registry = new ServiceRegistry(factories); - registry.#checkForCircularDeps(); + registry.checkForCircularDeps(); return registry; } @@ -80,6 +79,8 @@ export class ServiceRegistry implements EnumerableServiceHolder { InternalServiceFactory, Promise >(); + readonly #addedFactoryIds = new Set(); + readonly #instantiatedFactories = new Set(); private constructor(factories: Array) { this.#providedFactories = new Map( @@ -153,7 +154,7 @@ export class ServiceRegistry implements EnumerableServiceHolder { } } - #checkForCircularDeps(): void { + checkForCircularDeps(): void { const graph = DependencyGraph.fromIterable( Array.from(this.#providedFactories).map( ([serviceId, serviceFactory]) => ({ @@ -174,11 +175,37 @@ export class ServiceRegistry implements EnumerableServiceHolder { } } + add(factory: ServiceFactory) { + const factoryId = factory.service.id; + if (factoryId === coreServices.pluginMetadata.id) { + throw new Error( + `The ${coreServices.pluginMetadata.id} service cannot be overridden`, + ); + } + + if (this.#addedFactoryIds.has(factoryId)) { + throw new Error( + `Duplicate service implementations provided for ${factoryId}`, + ); + } + + if (this.#instantiatedFactories.has(factoryId)) { + throw new Error( + `Unable to set service factory with id ${factoryId}, service has already been instantiated`, + ); + } + + this.#addedFactoryIds.add(factoryId); + this.#providedFactories.set(factoryId, toInternalServiceFactory(factory)); + } + getServiceRefs(): ServiceRef[] { return Array.from(this.#providedFactories.values()).map(f => f.service); } get(ref: ServiceRef, pluginId: string): Promise | undefined { + this.#instantiatedFactories.add(ref.id); + return this.#resolveFactory(ref, pluginId)?.then(factory => { if (factory.service.scope === 'root') { let existing = this.#rootServiceImplementations.get(factory); diff --git a/packages/backend-app-api/src/wiring/types.ts b/packages/backend-app-api/src/wiring/types.ts index 552a69a792..adf8128b5b 100644 --- a/packages/backend-app-api/src/wiring/types.ts +++ b/packages/backend-app-api/src/wiring/types.ts @@ -37,17 +37,6 @@ export interface CreateSpecializedBackendOptions { defaultServiceFactories: ServiceFactoryOrFunction[]; } -export interface ServiceHolder { - get(api: ServiceRef, pluginId: string): Promise | undefined; -} - -/** - * @internal - */ -export interface EnumerableServiceHolder extends ServiceHolder { - getServiceRefs(): ServiceRef[]; -} - /** * @public */