From 54f2ac8c599980fd8ad722c93b8ccb30c5a8f916 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 2 Apr 2024 14:18:06 +0200 Subject: [PATCH 1/2] backend-app-api: add initialization option to createServiceFactory Co-authored-by: Patrik Oldsberg Signed-off-by: Vincenzo Scamporlino --- .changeset/silver-carrots-sort.md | 6 ++ .../src/wiring/BackendInitializer.test.ts | 101 ++++++++++++++---- .../src/wiring/BackendInitializer.ts | 12 ++- .../src/wiring/ServiceRegistry.ts | 16 ++- .../src/services/system/types.ts | 5 + 5 files changed, 113 insertions(+), 27 deletions(-) create mode 100644 .changeset/silver-carrots-sort.md diff --git a/.changeset/silver-carrots-sort.md b/.changeset/silver-carrots-sort.md new file mode 100644 index 0000000000..fef7f7098e --- /dev/null +++ b/.changeset/silver-carrots-sort.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-plugin-api': patch +'@backstage/backend-app-api': patch +--- + +Added `initialization` option to `createServiceFactory` which defines the initialization strategy for the service. The default strategy mimics the current behavior where plugin scoped services are initialized lazily by default and root scoped services are initialized eagerly. diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts index c025d19fa7..2e48dd234b 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts @@ -30,15 +30,6 @@ import { rootLifecycleServiceFactory, } from '../services/implementations'; -const rootRef = createServiceRef<{ x: number }>({ - id: '1', - scope: 'root', -}); - -const pluginRef = createServiceRef<{ x: number }>({ - id: '2', -}); - class MockLogger { debug() {} info() {} @@ -62,33 +53,103 @@ const baseFactories = [ describe('BackendInitializer', () => { it('should initialize root scoped services', async () => { - const rootFactory = jest.fn(); - const pluginFactory = jest.fn(); + const ref1 = createServiceRef<{ x: number }>({ + id: '1', + scope: 'root', + }); + const ref2 = createServiceRef<{ x: number }>({ + id: '2', + scope: 'root', + }); + const ref3 = createServiceRef<{ x: number }>({ + id: '3', + scope: 'root', + }); + const factory1 = jest.fn(); + const factory2 = jest.fn(); + const factory3 = jest.fn(); const services = [ + ...baseFactories, createServiceFactory({ - service: rootRef, + service: ref1, + initialization: 'always', deps: {}, - factory: rootFactory, + factory: factory1, })(), createServiceFactory({ - service: pluginRef, + service: ref2, deps: {}, - factory: pluginFactory, + factory: factory2, })(), - rootLifecycleServiceFactory(), createServiceFactory({ - service: coreServices.rootLogger, + service: ref3, + initialization: 'lazy', deps: {}, - factory: () => new MockLogger(), + factory: factory3, })(), ]; const init = new BackendInitializer(services); await init.start(); - expect(rootFactory).toHaveBeenCalled(); - expect(pluginFactory).not.toHaveBeenCalled(); + expect(factory1).toHaveBeenCalled(); + expect(factory2).toHaveBeenCalled(); + expect(factory3).not.toHaveBeenCalled(); + }); + + it('should initialize plugin scoped services with eager initialization', async () => { + const ref1 = createServiceRef<{ x: number }>({ + id: '1', + }); + const ref2 = createServiceRef<{ x: number }>({ + id: '2', + }); + const ref3 = createServiceRef<{ x: number }>({ + id: '3', + }); + const factory1 = jest.fn(); + const factory2 = jest.fn(); + const factory3 = jest.fn(); + + const services = [ + ...baseFactories, + createServiceFactory({ + service: ref1, + initialization: 'always', + deps: {}, + factory: factory1, + })(), + createServiceFactory({ + service: ref2, + deps: {}, + factory: factory2, + })(), + createServiceFactory({ + service: ref3, + initialization: 'lazy', + deps: {}, + factory: factory3, + })(), + ]; + + const init = new BackendInitializer(services); + init.add( + createBackendPlugin({ + pluginId: 'test', + register(reg) { + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + })(), + ); + await init.start(); + + expect(factory1).toHaveBeenCalled(); + expect(factory2).not.toHaveBeenCalled(); + expect(factory3).not.toHaveBeenCalled(); }); it('should initialize modules with extension points', async () => { diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index 2da3a5a226..f4b4fde804 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -170,11 +170,7 @@ export class BackendInitializer { } // Initialize all root scoped services - for (const ref of this.#serviceRegistry.getServiceRefs()) { - if (ref.scope === 'root') { - await this.#serviceRegistry.get(ref, 'root'); - } - } + await this.#serviceRegistry.initializeEagerServicesWithScope('root'); const pluginInits = new Map(); const moduleInits = new Map>(); @@ -235,6 +231,12 @@ export class BackendInitializer { // All plugins are initialized in parallel await Promise.all( allPluginIds.map(async pluginId => { + // Initialize all eager services + await this.#serviceRegistry.initializeEagerServicesWithScope( + 'plugin', + pluginId, + ); + // Modules are initialized before plugins, so that they can provide extension to the plugin const modules = moduleInits.get(pluginId); if (modules) { diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.ts index ad52e645bb..6e20dd98e8 100644 --- a/packages/backend-app-api/src/wiring/ServiceRegistry.ts +++ b/packages/backend-app-api/src/wiring/ServiceRegistry.ts @@ -199,8 +199,20 @@ export class ServiceRegistry { this.#providedFactories.set(factoryId, toInternalServiceFactory(factory)); } - getServiceRefs(): ServiceRef[] { - return Array.from(this.#providedFactories.values()).map(f => f.service); + async initializeEagerServicesWithScope( + scope: 'root' | 'plugin', + pluginId: string = 'root', + ) { + for (const factory of this.#providedFactories.values()) { + if (factory.service.scope === scope) { + // Root-scoped services are eager by default, plugin-scoped are lazy by default + if (scope === 'root' && factory.initialization !== 'lazy') { + await this.get(factory.service, pluginId); + } else if (scope === 'plugin' && factory.initialization === 'always') { + await this.get(factory.service, pluginId); + } + } + } } get(ref: ServiceRef, pluginId: string): Promise | undefined { diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts index 329502369e..9e751cbd39 100644 --- a/packages/backend-plugin-api/src/services/system/types.ts +++ b/packages/backend-plugin-api/src/services/system/types.ts @@ -63,6 +63,7 @@ export interface InternalServiceFactory< TScope extends 'plugin' | 'root' = 'plugin' | 'root', > extends ServiceFactory { version: 'v1'; + initialization?: 'always' | 'lazy'; deps: { [key in string]: ServiceRef }; createRootContext?(deps: { [key in string]: unknown }): Promise; factory( @@ -140,6 +141,7 @@ export interface RootServiceFactoryConfig< TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, > { + initialization?: 'always' | 'lazy'; service: ServiceRef; deps: TDeps; factory(deps: ServiceRefsToInstances): TImpl | Promise; @@ -152,6 +154,7 @@ export interface PluginServiceFactoryConfig< TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, > { + initialization?: 'always' | 'lazy'; service: ServiceRef; deps: TDeps; createRootContext?( @@ -251,6 +254,7 @@ export function createServiceFactory< $$type: '@backstage/BackendFeature', version: 'v1', service: c.service, + initialization: c.initialization, deps: c.deps, factory: async (deps: TDeps) => c.factory(deps), }; @@ -265,6 +269,7 @@ export function createServiceFactory< $$type: '@backstage/BackendFeature', version: 'v1', service: c.service, + initialization: c.initialization, ...('createRootContext' in c ? { createRootContext: async (deps: TDeps) => From 5a265dfe9a00d4ba1b71e9c6c3d1991da2ead48b Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 2 Apr 2024 23:33:28 +0200 Subject: [PATCH 2/2] backend-plugin-api: add initialization to api-report Signed-off-by: Vincenzo Scamporlino --- packages/backend-plugin-api/api-report.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index 73c1d15cd1..6f6f630850 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -439,6 +439,8 @@ export interface PluginServiceFactoryConfig< context: TContext, ): TImpl | Promise; // (undocumented) + initialization?: 'always' | 'lazy'; + // (undocumented) service: ServiceRef; } @@ -518,6 +520,8 @@ export interface RootServiceFactoryConfig< // (undocumented) factory(deps: ServiceRefsToInstances): TImpl | Promise; // (undocumented) + initialization?: 'always' | 'lazy'; + // (undocumented) service: ServiceRef; }