chore(modules-sdk): load modules in sequence (#9327)

What:
- The time taken to load in sequence is the same as in parallel, and it doesn't create multiple db queries simultaneously when starting each module.
- Rework modules bootstrap (now all dependencies are available from the constructor and cross deps are allowed without any topological sort needed. It also allow improvements in the future)
  - First load all modules
  - then resolve and register instances


Co-authored-by: Adrien de Peretti <25098370+adrien2p@users.noreply.github.com>
This commit is contained in:
Carlos R. L. Rodrigues
2024-09-26 13:33:35 -03:00
committed by GitHub
parent 6854feaf50
commit ef15c60386
2 changed files with 349 additions and 154 deletions
+93 -77
View File
@@ -32,6 +32,7 @@ import { MODULE_PACKAGE_NAMES } from "./definitions"
import {
MedusaModule,
MigrationOptions,
ModuleBootstrapOptions,
RegisterModuleJoinerConfig,
} from "./medusa-module"
import { RemoteLink } from "./remote-link"
@@ -97,89 +98,104 @@ export async function loadModules(args: {
sharedResourcesConfig,
migrationOnly = false,
loaderOnly = false,
workerMode = "server",
workerMode = "server" as ModuleBootstrapOptions["workerMode"],
} = args
const allModules = {}
const allModules = {} as any
await Promise.all(
Object.keys(modulesConfig).map(async (moduleName) => {
const mod = modulesConfig[moduleName]
let path: string
let moduleExports: ModuleExports | undefined = undefined
let declaration: any = {}
let definition: Partial<ModuleDefinition> | undefined = undefined
const modulesToLoad: {
moduleKey: string
defaultPath: string
declaration: InternalModuleDeclaration | ExternalModuleDeclaration
sharedContainer: MedusaContainer
moduleDefinition: ModuleDefinition
moduleExports?: ModuleExports
}[] = []
// Skip disabled modules
if (mod === false) {
return
for (const moduleName of Object.keys(modulesConfig)) {
const mod = modulesConfig[moduleName]
let path: string
let moduleExports: ModuleExports | undefined = undefined
let declaration: any = {}
let definition: Partial<ModuleDefinition> | undefined = undefined
if (mod === false) {
continue
}
if (isObject(mod)) {
const mod_ = mod as unknown as InternalModuleDeclaration
path = mod_.resolve ?? MODULE_PACKAGE_NAMES[moduleName]
definition = mod_.definition
moduleExports = !isString(mod_.resolve)
? (mod_.resolve as ModuleExports)
: undefined
declaration = { ...mod }
delete declaration.definition
} else {
path = MODULE_PACKAGE_NAMES[moduleName]
}
declaration.scope ??= MODULE_SCOPE.INTERNAL
if (declaration.scope === MODULE_SCOPE.INTERNAL && !declaration.resources) {
declaration.resources = MODULE_RESOURCE_TYPE.SHARED
}
if (
declaration.scope === MODULE_SCOPE.INTERNAL &&
declaration.resources === MODULE_RESOURCE_TYPE.SHARED
) {
declaration.options ??= {}
declaration.options.database ??= {
...sharedResourcesConfig?.database,
}
declaration.options.database.debug ??=
sharedResourcesConfig?.database?.debug
}
if (isObject(mod)) {
const mod_ = mod as unknown as InternalModuleDeclaration
path = mod_.resolve ?? MODULE_PACKAGE_NAMES[moduleName]
definition = mod_.definition
moduleExports = !isString(mod_.resolve)
? (mod_.resolve as ModuleExports)
: undefined
declaration = { ...mod }
delete declaration.definition
} else {
path = MODULE_PACKAGE_NAMES[moduleName]
}
declaration.scope ??= MODULE_SCOPE.INTERNAL
if (
declaration.scope === MODULE_SCOPE.INTERNAL &&
!declaration.resources
) {
declaration.resources = MODULE_RESOURCE_TYPE.SHARED
}
if (
declaration.scope === MODULE_SCOPE.INTERNAL &&
declaration.resources === MODULE_RESOURCE_TYPE.SHARED
) {
declaration.options ??= {}
declaration.options.database ??= {
...sharedResourcesConfig?.database,
}
declaration.options.database.debug ??=
sharedResourcesConfig?.database?.debug
}
const loaded = (await MedusaModule.bootstrap({
moduleKey: moduleName,
defaultPath: path,
declaration,
sharedContainer,
moduleDefinition: definition as ModuleDefinition,
moduleExports,
migrationOnly,
loaderOnly,
workerMode,
})) as LoadedModule
if (loaderOnly) {
return
}
const service = loaded[moduleName]
sharedContainer.register({
[service.__definition.key]: asValue(service),
})
if (allModules[moduleName] && !Array.isArray(allModules[moduleName])) {
allModules[moduleName] = []
}
if (allModules[moduleName]) {
;(allModules[moduleName] as LoadedModule[]).push(loaded[moduleName])
} else {
allModules[moduleName] = loaded[moduleName]
}
modulesToLoad.push({
moduleKey: moduleName,
defaultPath: path,
declaration,
sharedContainer,
moduleDefinition: definition as ModuleDefinition,
moduleExports,
})
)
}
const loaded = (await MedusaModule.bootstrapAll(modulesToLoad, {
migrationOnly,
loaderOnly,
workerMode,
})) as LoadedModule[]
if (loaderOnly) {
return allModules
}
for (const { moduleKey } of modulesToLoad) {
const service = loaded.find((loadedModule) => loadedModule[moduleKey])?.[
moduleKey
]
if (!service) {
throw new Error(`Module ${moduleKey} could not be loaded.`)
}
sharedContainer.register({
[service.__definition.key]: asValue(service),
})
if (allModules[moduleKey] && !Array.isArray(allModules[moduleKey])) {
allModules[moduleKey] = []
}
if (allModules[moduleKey]) {
;(allModules[moduleKey] as LoadedModule[]).push(service)
} else {
allModules[moduleKey] = service
}
}
return allModules
}
@@ -374,7 +390,7 @@ async function MedusaApp_({
const allModules = await loadModules({
modulesConfig: modules,
sharedContainer: sharedContainer_,
sharedResourcesConfig,
sharedResourcesConfig: { database: dbData },
migrationOnly,
loaderOnly,
workerMode,
+256 -77
View File
@@ -273,6 +273,52 @@ class MedusaModule {
MedusaModule.modules_.set(moduleKey, modules!)
}
/**
* Load all modules and resolve them once they are loaded
* @param modulesOptions
* @param migrationOnly
* @param loaderOnly
* @param workerMode
*/
public static async bootstrapAll(
modulesOptions: Omit<
ModuleBootstrapOptions,
"migrationOnly" | "loaderOnly" | "workerMode"
>[],
{
migrationOnly,
loaderOnly,
workerMode,
}: {
migrationOnly?: boolean
loaderOnly?: boolean
workerMode?: ModuleBootstrapOptions["workerMode"]
}
): Promise<
{
[key: string]: any
}[]
> {
return await MedusaModule.bootstrap_(modulesOptions, {
migrationOnly,
loaderOnly,
workerMode,
})
}
/**
* Load a single module and resolve it once it is loaded
* @param moduleKey
* @param defaultPath
* @param declaration
* @param moduleExports
* @param sharedContainer
* @param moduleDefinition
* @param injectedDependencies
* @param migrationOnly
* @param loaderOnly
* @param workerMode
*/
public static async bootstrap<T>({
moduleKey,
defaultPath,
@@ -287,96 +333,232 @@ class MedusaModule {
}: ModuleBootstrapOptions): Promise<{
[key: string]: T
}> {
const hashKey = simpleHash(
stringifyCircular({ moduleKey, defaultPath, declaration })
const [service] = await MedusaModule.bootstrap_(
[
{
moduleKey,
defaultPath,
declaration,
moduleExports,
sharedContainer,
moduleDefinition,
injectedDependencies,
},
],
{
migrationOnly,
loaderOnly,
workerMode,
}
)
if (!loaderOnly && MedusaModule.instances_.has(hashKey)) {
return MedusaModule.instances_.get(hashKey)! as {
[key: string]: T
return service as {
[key: string]: T
}
}
/**
* Load all modules and then resolve them once they are loaded
*
* @param modulesOptions
* @param migrationOnly
* @param loaderOnly
* @param workerMode
* @protected
*/
protected static async bootstrap_<T>(
modulesOptions: Omit<
ModuleBootstrapOptions,
"migrationOnly" | "loaderOnly" | "workerMode"
>[],
{
migrationOnly,
loaderOnly,
workerMode,
}: {
migrationOnly?: boolean
loaderOnly?: boolean
workerMode?: "shared" | "worker" | "server"
}
): Promise<
{
[key: string]: T
}[]
> {
let loadedModules: {
hashKey: string
modDeclaration: InternalModuleDeclaration | ExternalModuleDeclaration
moduleResolutions: Record<string, ModuleResolution>
container: MedusaContainer
finishLoading: (arg: { [Key: string]: any }) => void
}[] = []
const services: { [Key: string]: any }[] = []
for (const moduleOptions of modulesOptions) {
const {
moduleKey,
defaultPath,
declaration,
moduleExports,
sharedContainer,
moduleDefinition,
injectedDependencies,
} = moduleOptions
const hashKey = simpleHash(
stringifyCircular({ moduleKey, defaultPath, declaration })
)
let finishLoading: any
let errorLoading: any
const loadingPromise = new Promise((resolve, reject) => {
finishLoading = resolve
errorLoading = reject
})
if (!loaderOnly && MedusaModule.instances_.has(hashKey)) {
services.push(MedusaModule.instances_.get(hashKey)!)
continue
}
}
if (!loaderOnly && MedusaModule.loading_.has(hashKey)) {
return MedusaModule.loading_.get(hashKey)
}
let finishLoading: any
let errorLoading: any
const loadingPromise = new Promise((resolve, reject) => {
finishLoading = resolve
errorLoading = reject
})
if (!loaderOnly) {
MedusaModule.loading_.set(hashKey, loadingPromise)
}
let modDeclaration =
declaration ??
({} as InternalModuleDeclaration | ExternalModuleDeclaration)
if (declaration?.scope !== MODULE_SCOPE.EXTERNAL) {
modDeclaration = {
scope: declaration?.scope || MODULE_SCOPE.INTERNAL,
resources: declaration?.resources || MODULE_RESOURCE_TYPE.ISOLATED,
resolve: defaultPath,
options: declaration?.options ?? declaration,
dependencies: declaration?.dependencies ?? [],
alias: declaration?.alias,
main: declaration?.main,
worker_mode: workerMode,
if (!loaderOnly && MedusaModule.loading_.has(hashKey)) {
services.push(await MedusaModule.loading_.get(hashKey))
continue
}
}
// TODO: Only do that while legacy modules sharing the manager exists then remove the ternary in favor of createMedusaContainer({}, globalContainer)
const container =
modDeclaration.scope === MODULE_SCOPE.INTERNAL &&
modDeclaration.resources === MODULE_RESOURCE_TYPE.SHARED
? sharedContainer ?? createMedusaContainer()
: createMedusaContainer({}, sharedContainer)
if (!loaderOnly) {
MedusaModule.loading_.set(hashKey, loadingPromise)
}
if (injectedDependencies) {
for (const service in injectedDependencies) {
container.register(service, asValue(injectedDependencies[service]))
if (!container.hasRegistration(service)) {
let modDeclaration =
declaration ??
({} as InternalModuleDeclaration | ExternalModuleDeclaration)
if (declaration?.scope !== MODULE_SCOPE.EXTERNAL) {
modDeclaration = {
scope: declaration?.scope || MODULE_SCOPE.INTERNAL,
resources:
(declaration as InternalModuleDeclaration)?.resources ||
MODULE_RESOURCE_TYPE.ISOLATED,
resolve: defaultPath,
options: declaration?.options ?? declaration,
dependencies:
(declaration as InternalModuleDeclaration)?.dependencies ?? [],
alias: declaration?.alias,
main: declaration?.main,
worker_mode: workerMode,
} as any
}
// TODO: Only do that while legacy modules sharing the manager exists then remove the ternary in favor of createMedusaContainer({}, globalContainer)
const container =
modDeclaration.scope === MODULE_SCOPE.INTERNAL &&
modDeclaration.resources === MODULE_RESOURCE_TYPE.SHARED
? sharedContainer ?? createMedusaContainer()
: createMedusaContainer({}, sharedContainer)
if (injectedDependencies) {
for (const service in injectedDependencies) {
container.register(service, asValue(injectedDependencies[service]))
if (!container.hasRegistration(service)) {
container.register(service, asValue(injectedDependencies[service]))
}
}
}
const moduleResolutions = registerMedusaModule(
moduleKey,
modDeclaration!,
moduleExports,
moduleDefinition
)
const logger_ =
container.resolve(ContainerRegistrationKeys.LOGGER, {
allowUnregistered: true,
}) ?? logger
try {
await moduleLoader({
container,
moduleResolutions,
logger: logger_,
migrationOnly,
loaderOnly,
})
} catch (err) {
errorLoading(err)
throw err
}
loadedModules.push({
hashKey,
modDeclaration,
moduleResolutions,
container,
finishLoading,
})
}
const moduleResolutions = registerMedusaModule(
moduleKey,
modDeclaration!,
moduleExports,
moduleDefinition
)
if (loaderOnly) {
loadedModules.forEach(({ finishLoading }) => finishLoading({}))
return [{}]
}
for (const {
hashKey,
modDeclaration,
moduleResolutions,
container,
finishLoading,
} of loadedModules) {
const service = await MedusaModule.resolveLoadedModule({
hashKey,
modDeclaration,
moduleResolutions,
container,
})
MedusaModule.instances_.set(hashKey, service)
finishLoading(service)
MedusaModule.loading_.delete(hashKey)
services.push(service)
}
return services
}
/**
* Resolve all the modules once they all have been loaded through the bootstrap
* and store their references in the instances_ map and return them
*
* @param hashKey
* @param modDeclaration
* @param moduleResolutions
* @param container
* @private
*/
private static async resolveLoadedModule({
hashKey,
modDeclaration,
moduleResolutions,
container,
}: {
hashKey: string
modDeclaration: InternalModuleDeclaration | ExternalModuleDeclaration
moduleResolutions: Record<string, ModuleResolution>
container: MedusaContainer
}): Promise<{
[key: string]: any
}> {
const logger_ =
container.resolve(ContainerRegistrationKeys.LOGGER, {
allowUnregistered: true,
}) ?? logger
try {
await moduleLoader({
container,
moduleResolutions,
logger: logger_,
migrationOnly,
loaderOnly,
})
} catch (err) {
errorLoading(err)
throw err
}
const services = {}
if (loaderOnly) {
finishLoading(services)
return services
}
const services: { [key: string]: any } = {}
for (const resolution of Object.values(
moduleResolutions
@@ -390,6 +572,7 @@ class MedusaModule {
let joinerConfig!: ModuleJoinerConfig
try {
// TODO: rework that to store on a separate property
joinerConfig = await services[keyName].__joinerConfig?.()
} catch {
// noop
@@ -424,10 +607,6 @@ class MedusaModule {
})
}
MedusaModule.instances_.set(hashKey, services)
finishLoading(services)
MedusaModule.loading_.delete(hashKey)
return services
}
@@ -447,7 +626,7 @@ class MedusaModule {
}
if (MedusaModule.loading_.has(hashKey)) {
return MedusaModule.loading_.get(hashKey)
return await MedusaModule.loading_.get(hashKey)
}
let finishLoading: any