backend-app-api: make sure modules are initialized in the correct order

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-08-10 19:10:13 +02:00
parent c7aa4ff179
commit 8b85f546f7
2 changed files with 115 additions and 15 deletions
@@ -21,7 +21,7 @@ import {
createBackendPlugin,
createBackendModule,
} from '@backstage/backend-plugin-api';
import { BackendInitializer } from './BackendInitializer';
import { BackendInitializer, resolveInitChunks } from './BackendInitializer';
import { ServiceRegistry } from './ServiceRegistry';
import { rootLifecycleServiceFactory } from '../services/implementations';
@@ -176,3 +176,49 @@ describe('BackendInitializer', () => {
);
});
});
describe('resolveInitChunks', () => {
it('should resolve flat chunks', () => {
const resolved = resolveInitChunks(
new Map(
Object.entries({
a: { consumes: new Set(), provides: new Set(['a']) } as any,
b: { consumes: new Set(), provides: new Set(['b']) } as any,
c: { consumes: new Set(), provides: new Set(['c']) } as any,
d: { consumes: new Set(), provides: new Set(['d']) } as any,
}),
),
).map(chunk => Array.from(chunk.keys()));
expect(resolved).toEqual([['a', 'b', 'c', 'd']]);
});
it('should resolve nested chunks', () => {
const resolved = resolveInitChunks(
new Map(
Object.entries({
a: { consumes: new Set(), provides: new Set(['a']) } as any,
b: { consumes: new Set(['a']), provides: new Set(['b', 'c']) } as any,
c: { consumes: new Set(['b']), provides: new Set() } as any,
d: { consumes: new Set(['c']), provides: new Set() } as any,
}),
),
).map(chunk => Array.from(chunk.keys()));
expect(resolved).toEqual([['a'], ['b'], ['c', 'd']]);
});
it('should detect circular dependencies', () => {
expect(() =>
resolveInitChunks(
new Map(
Object.entries({
a: { consumes: new Set(), provides: new Set(['a']) } as any,
b: { consumes: new Set(['a', 'b']), provides: new Set() } as any,
c: { consumes: new Set(['a', 'c']), provides: new Set() } as any,
}),
),
),
).toThrow(
"Failed to resolve module initialization order, the following modules have circular dependencies, 'b', 'c'",
);
});
});
@@ -38,6 +38,58 @@ export interface BackendRegisterInit {
};
}
/** @internal */
export function resolveInitChunks(
registerInits: Map<string, BackendRegisterInit>,
): Map<string, BackendRegisterInit>[] {
let registerInitsToOrder = Array.from(registerInits);
const orderedRegisterInits = new Array<Map<string, BackendRegisterInit>>();
// Keep track of all deps that have been provided so far
const provided = new Set<ServiceOrExtensionPoint>();
// Loop until we have ordered all registerInits
while (registerInitsToOrder.length > 0) {
const chunk = new Map<string, BackendRegisterInit>();
// Find all registerInits that have all their deps provided
for (const [id, registerInit] of registerInitsToOrder) {
if (
Array.from(registerInit.consumes).every(consumed =>
provided.has(consumed),
)
) {
chunk.set(id, registerInit);
}
}
// Add to the result set
orderedRegisterInits.push(chunk);
// Register all the deps in the current chunk as provided
chunk.forEach(r => r.provides.forEach(p => provided.add(p)));
// Remove the current chunk from the registerInits to order
const newRegisterInitsToOrder = registerInitsToOrder.filter(
([id]) => !chunk.has(id),
);
// Check that we have not hit a circular dependency
if (
registerInitsToOrder.length !== 0 &&
registerInitsToOrder.length === newRegisterInitsToOrder.length
) {
throw new Error(
`Failed to resolve module initialization order, the following modules have circular dependencies, '${registerInitsToOrder
.map(r => r[0])
.join("', '")}'`,
);
}
registerInitsToOrder = newRegisterInitsToOrder;
}
return orderedRegisterInits;
}
export class BackendInitializer {
#startPromise?: Promise<void>;
#features = new Array<InternalBackendFeature>();
@@ -210,21 +262,23 @@ export class BackendInitializer {
await Promise.all(
allPluginIds.map(async pluginId => {
// Modules are initialized before plugins, so that they can provide extension to the plugin
const modules = moduleInits.get(pluginId) ?? [];
await Promise.all(
Array.from(modules).map(async ([moduleId, moduleInit]) => {
const moduleDeps = await this.#getInitDeps(
moduleInit.init.deps,
pluginId,
);
await moduleInit.init.func(moduleDeps).catch(error => {
throw new ForwardedError(
`Module '${moduleId}' for plugin '${pluginId}' startup failed`,
error,
const modules = moduleInits.get(pluginId) ?? new Map();
for (const chunk of resolveInitChunks(modules)) {
await Promise.all(
Array.from(chunk).map(async ([moduleId, moduleInit]) => {
const moduleDeps = await this.#getInitDeps(
moduleInit.init.deps,
pluginId,
);
});
}),
);
await moduleInit.init.func(moduleDeps).catch(error => {
throw new ForwardedError(
`Module '${moduleId}' for plugin '${pluginId}' startup failed`,
error,
);
});
}),
);
}
// Once all modules have been initialized, we can initialize the plugin itself
const pluginInit = pluginInits.get(pluginId);