chore: medusa shutdown (#6865)

* chore: medusa shutdown

* continue

* use shutdown

* on application shutdown

* consume shutdown

* more connection close

* more cleanup

* more cleanup

* update lock

* revert package

* graceful shutdown

* Create yellow-apples-attack.md

* graceful shutdown

* graceful shutdown

---------

Co-authored-by: Sebastian Rindom <skrindom@gmail.com>
Co-authored-by: Riqwan Thamir <rmthamir@gmail.com>
This commit is contained in:
authored and GitHub committed 2024-03-29 09:22:29 -03:00
1 parent 0c0b425de7
commit 8fd1488938
20 files changed
+225 -67

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
---
"@medusajs/medusa": patch
"@medusajs/cache-redis": patch
"@medusajs/event-bus-redis": patch
"medusa-test-utils": patch
"@medusajs/modules-sdk": patch
"@medusajs/types": patch
"@medusajs/workflow-engine-redis": patch
---
chore: medusa shutdown
@@ -23,6 +23,13 @@ class RedisCacheService implements ICacheService {
this.TTL = options.ttl ?? DEFAULT_CACHE_TIME this.TTL = options.ttl ?? DEFAULT_CACHE_TIME
this.namespace = options.namespace || DEFAULT_NAMESPACE this.namespace = options.namespace || DEFAULT_NAMESPACE
} }
__hooks = {
onApplicationShutdown: async () => {
this.redis.disconnect()
},
}
/** /**
* Set a key/value pair to the cache. * Set a key/value pair to the cache.
* If the ttl is 0 it will act like the value should not be cached at all. * If the ttl is 0 it will act like the value should not be cached at all.
@@ -20,8 +20,10 @@ export default class RedisEventBusService extends AbstractEventBusModuleService
protected readonly moduleOptions_: EventBusRedisModuleOptions protected readonly moduleOptions_: EventBusRedisModuleOptions
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
protected readonly moduleDeclaration_: InternalModuleDeclaration protected readonly moduleDeclaration_: InternalModuleDeclaration
protected readonly eventBusRedisConnection_: Redis
protected queue_: Queue protected queue_: Queue
protected bullWorker_: Worker
constructor( constructor(
{ logger, eventBusRedisConnection }: InjectedDependencies, { logger, eventBusRedisConnection }: InjectedDependencies,
@@ -32,6 +34,8 @@ export default class RedisEventBusService extends AbstractEventBusModuleService
// eslint-disable-next-line prefer-rest-params // eslint-disable-next-line prefer-rest-params
super(...arguments) super(...arguments)
this.eventBusRedisConnection_ = eventBusRedisConnection
this.moduleOptions_ = moduleOptions this.moduleOptions_ = moduleOptions
this.logger_ = logger this.logger_ = logger
@@ -44,12 +48,24 @@ export default class RedisEventBusService extends AbstractEventBusModuleService
// Register our worker to handle emit calls // Register our worker to handle emit calls
const shouldStartWorker = moduleDeclaration.worker_mode !== "server" const shouldStartWorker = moduleDeclaration.worker_mode !== "server"
if (shouldStartWorker) { if (shouldStartWorker) {
new Worker(moduleOptions.queueName ?? "events-queue", this.worker_, { this.bullWorker_ = new Worker(
moduleOptions.queueName ?? "events-queue",
this.worker_,
{
prefix: `${this.constructor.name}`, prefix: `${this.constructor.name}`,
...(moduleOptions.workerOptions ?? {}), ...(moduleOptions.workerOptions ?? {}),
connection: eventBusRedisConnection, connection: eventBusRedisConnection,
})
} }
)
}
}
__hooks = {
onApplicationShutdown: async () => {
await this.bullWorker_.close(true)
await this.queue_.close()
this.eventBusRedisConnection_.disconnect()
},
} }
/** /**
+10 -3
View File
@@ -4,7 +4,11 @@ import {
MedusaModuleConfig, MedusaModuleConfig,
ModuleJoinerConfig, ModuleJoinerConfig,
} from "@medusajs/modules-sdk" } from "@medusajs/modules-sdk"
import { ContainerRegistrationKeys, ModulesSdkUtils } from "@medusajs/utils" import {
ContainerRegistrationKeys,
ModulesSdkUtils,
promiseAll,
} from "@medusajs/utils"
export interface InitModulesOptions { export interface InitModulesOptions {
injectedDependencies?: Record<string, unknown> injectedDependencies?: Record<string, unknown>
@@ -48,8 +52,11 @@ export async function initModules({
async function shutdown() { async function shutdown() {
if (shouldDestroyConnectionAutomatically) { if (shouldDestroyConnectionAutomatically) {
await (sharedPgConnection as any).context?.destroy() await promiseAll([
await (sharedPgConnection as any).destroy() (sharedPgConnection as any).context?.destroy(),
(sharedPgConnection as any).destroy(),
medusaApp.onApplicationShutdown(),
])
} else { } else {
if (!preventConnectionDestroyWarning) { if (!preventConnectionDestroyWarning) {
console.info( console.info(
@@ -1,7 +1,8 @@
const path = require("path") const path = require("path")
const express = require("express") const express = require("express")
const getPort = require("get-port") const getPort = require("get-port")
const { isObject } = require("@medusajs/utils") const { isObject, promiseAll } = require("@medusajs/utils")
const { GracefulShutdownServer } = require("medusa-core-utils")
async function bootstrapApp({ cwd, env = {} } = {}) { async function bootstrapApp({ cwd, env = {} } = {}) {
const app = express() const app = express()
@@ -12,8 +13,7 @@ async function bootstrapApp({ cwd, env = {} } = {}) {
const loaders = require("@medusajs/medusa/dist/loaders").default const loaders = require("@medusajs/medusa/dist/loaders").default
const { container, dbConnection, pgConnection, disposeResources } = const { container, shutdown } = await loaders({
await loaders({
directory: path.resolve(cwd || process.cwd()), directory: path.resolve(cwd || process.cwd()),
expressApp: app, expressApp: app,
isTest: false, isTest: false,
@@ -22,10 +22,8 @@ async function bootstrapApp({ cwd, env = {} } = {}) {
const PORT = await getPort() const PORT = await getPort()
return { return {
disposeResources, shutdown,
container, container,
db: dbConnection,
pgConnection,
app, app,
port: PORT, port: PORT,
} }
@@ -37,10 +35,16 @@ module.exports = {
env = {}, env = {},
skipExpressListen = false, skipExpressListen = false,
} = {}) => { } = {}) => {
const { app, port, container, db, pgConnection } = await bootstrapApp({ const {
app,
port,
container,
shutdown: medusaShutdown,
} = await bootstrapApp({
cwd, cwd,
env, env,
}) })
let expressServer let expressServer
if (skipExpressListen) { if (skipExpressListen) {
@@ -48,13 +52,7 @@ module.exports = {
} }
const shutdown = async () => { const shutdown = async () => {
await Promise.all([ await promiseAll([expressServer.shutdown(), medusaShutdown()])
container.dispose(),
expressServer.close(),
db?.destroy(),
pgConnection?.context?.destroy(),
container.dispose(),
])
if (typeof global !== "undefined" && global?.gc) { if (typeof global !== "undefined" && global?.gc) {
global.gc() global.gc()
@@ -62,7 +60,7 @@ module.exports = {
} }
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
expressServer = app.listen(port, async (err) => { const server = app.listen(port, async (err) => {
if (err) { if (err) {
await shutdown() await shutdown()
return reject(err) return reject(err)
@@ -74,6 +72,8 @@ module.exports = {
port, port,
}) })
}) })
expressServer = GracefulShutdownServer.create(server)
}) })
}, },
} }
@@ -54,6 +54,7 @@ const dbTestUtilFactory = (): any => ({
shutdown: async function (dbName: string) { shutdown: async function (dbName: string) {
await this.db_?.destroy() await this.db_?.destroy()
await this.pgConnection_?.context?.destroy() await this.pgConnection_?.context?.destroy()
await this.pgConnection_?.destroy()
return await dropDatabase( return await dropDatabase(
{ databaseName: dbName, errorIfNonExist: false }, { databaseName: dbName, errorIfNonExist: false },
@@ -116,7 +117,7 @@ export function medusaIntegrationTestRunner({
const cwd = process.cwd() const cwd = process.cwd()
let shutdown: () => Promise<void> let shutdown = async () => void 0
let dbUtils = dbTestUtilFactory() let dbUtils = dbTestUtilFactory()
let container: ContainerLike let container: ContainerLike
let apiUtils: any let apiUtils: any
@@ -142,6 +143,8 @@ export function medusaIntegrationTestRunner({
getContainer: () => container, getContainer: () => container,
} as MedusaSuiteOptions } as MedusaSuiteOptions
let isFirstTime = true
const beforeAll_ = async () => { const beforeAll_ = async () => {
await dbUtils.create(dbName) await dbUtils.create(dbName)
const { dbDataSource, pgConnection } = await initDb({ const { dbDataSource, pgConnection } = await initDb({
@@ -156,7 +159,7 @@ export function medusaIntegrationTestRunner({
dbUtils.pgConnection_ = pgConnection dbUtils.pgConnection_ = pgConnection
const { const {
shutdown: shutdown_, shutdown: serverShutdown,
container: container_, container: container_,
port, port,
} = await startBootstrapApp({ } = await startBootstrapApp({
@@ -164,13 +167,26 @@ export function medusaIntegrationTestRunner({
env, env,
}) })
apiUtils = axios.create({ baseURL: `http://localhost:${port}` }) const cancelTokenSource = axios.CancelToken.source()
apiUtils = axios.create({
baseURL: `http://localhost:${port}`,
cancelToken: cancelTokenSource.token,
})
container = container_ container = container_
shutdown = shutdown_ shutdown = async () => {
await serverShutdown()
cancelTokenSource.cancel("Request canceled by shutdown")
}
} }
const beforeEach_ = async () => { const beforeEach_ = async () => {
// The beforeAll already run everything, so lets not re run the loaders for the first iteration
if (isFirstTime) {
isFirstTime = false
return
}
const container = options.getContainer() const container = options.getContainer()
const copiedContainer = createMedusaContainer({}, container) const copiedContainer = createMedusaContainer({}, container)
@@ -54,7 +54,10 @@ export default async function ({ port, cpus, directory }) {
const app = express() const app = express()
const { dbConnection } = await loaders({ directory, expressApp: app }) const { dbConnection, shutdown } = await loaders({
directory,
expressApp: app,
})
const serverActivity = Logger.activity(`Creating server`) const serverActivity = Logger.activity(`Creating server`)
const server = GracefulShutdownServer.create( const server = GracefulShutdownServer.create(
app.listen(port, (err) => { app.listen(port, (err) => {
@@ -70,8 +73,10 @@ export default async function ({ port, cpus, directory }) {
server server
.shutdown() .shutdown()
.then(() => { .then(() => {
shutdown().then(() => {
process.exit(0) process.exit(0)
}) })
})
.catch((e) => { .catch((e) => {
process.exit(1) process.exit(1)
}) })
+4 -3
View File
@@ -19,14 +19,13 @@ export default async function ({ port, directory }) {
const app = express() const app = express()
try { try {
const { dbConnection, configModule, container } = await loaders({ const { dbConnection, shutdown } = await loaders({
directory, directory,
expressApp: app, expressApp: app,
}) })
let server
const serverActivity = Logger.activity(`Creating server`) const serverActivity = Logger.activity(`Creating server`)
server = GracefulShutdownServer.create( const server = GracefulShutdownServer.create(
app.listen(port, (err) => { app.listen(port, (err) => {
if (err) { if (err) {
return return
@@ -42,8 +41,10 @@ export default async function ({ port, directory }) {
.shutdown() .shutdown()
.then(() => { .then(() => {
Logger.info("Gracefully stopping the server.") Logger.info("Gracefully stopping the server.")
shutdown().then(() => {
process.exit(0) process.exit(0)
}) })
})
.catch((e) => { .catch((e) => {
Logger.error("Error received when shutting down the server.", e) Logger.error("Error received when shutting down the server.", e)
process.exit(1) process.exit(1)
+12 -3
View File
@@ -11,7 +11,10 @@ type Options = {
configModule: ConfigModule configModule: ConfigModule
} }
export default async ({ app, configModule }: Options): Promise<Express> => { export default async ({ app, configModule }: Options): Promise<{
app: Express,
shutdown: () => Promise<void>
}> => {
let sameSite: string | boolean = false let sameSite: string | boolean = false
let secure = false let secure = false
if ( if (
@@ -38,9 +41,11 @@ export default async ({ app, configModule }: Options): Promise<Express> => {
store: null, store: null,
} }
let redisClient
if (configModule?.projectConfig?.redis_url) { if (configModule?.projectConfig?.redis_url) {
const RedisStore = createStore(session) const RedisStore = createStore(session)
const redisClient = new Redis( redisClient = new Redis(
configModule.projectConfig.redis_url, configModule.projectConfig.redis_url,
configModule.projectConfig.redis_options ?? {} configModule.projectConfig.redis_options ?? {}
) )
@@ -63,5 +68,9 @@ export default async ({ app, configModule }: Options): Promise<Express> => {
res.status(200).send("OK") res.status(200).send("OK")
}) })
return app const shutdown = async () => {
redisClient?.disconnect()
}
return { app, shutdown }
} }
+34 -8
View File
@@ -6,8 +6,8 @@ import {
import { ConfigModule, MODULE_RESOURCE_TYPE } from "@medusajs/types" import { ConfigModule, MODULE_RESOURCE_TYPE } from "@medusajs/types"
import { import {
ContainerRegistrationKeys, ContainerRegistrationKeys,
MedusaV2Flag,
isString, isString,
MedusaV2Flag, promiseAll,
} from "@medusajs/utils" } from "@medusajs/utils"
import { asValue } from "awilix" import { asValue } from "awilix"
import { Express, NextFunction, Request, Response } from "express" import { Express, NextFunction, Request, Response } from "express"
@@ -97,18 +97,21 @@ async function loadMedusaV2({
container.register({ container.register({
[ContainerRegistrationKeys.LOGGER]: asValue(Logger), [ContainerRegistrationKeys.LOGGER]: asValue(Logger),
featureFlagRouter: asValue(featureFlagRouter), [ContainerRegistrationKeys.FEATURE_FLAG_ROUTER]: asValue(featureFlagRouter),
[ContainerRegistrationKeys.CONFIG_MODULE]: asValue(configModule), [ContainerRegistrationKeys.CONFIG_MODULE]: asValue(configModule),
["remoteQuery"]: asValue(null), [ContainerRegistrationKeys.REMOTE_QUERY]: asValue(null),
}) })
await loadMedusaApp({ const { onApplicationShutdown: medusaAppOnApplicationShutdown } = await loadMedusaApp({
configModule, configModule,
container, container,
}) })
let expressShutdown = async () => {}
if (shouldStartAPI) { if (shouldStartAPI) {
await expressLoader({ app: expressApp, configModule }) const { shutdown } = await expressLoader({ app: expressApp, configModule })
expressShutdown = shutdown
expressApp.use((req: Request, res: Response, next: NextFunction) => { expressApp.use((req: Request, res: Response, next: NextFunction) => {
req.scope = container.createScope() as MedusaContainer req.scope = container.createScope() as MedusaContainer
@@ -145,11 +148,21 @@ async function loadMedusaV2({
await createDefaultsWorkflow(container).run() await createDefaultsWorkflow(container).run()
const shutdown = async () => {
await promiseAll([
container.dispose(),
pgConnection?.context?.destroy(),
expressShutdown(),
medusaAppOnApplicationShutdown()
])
}
return { return {
configModule, configModule,
container, container,
app: expressApp, app: expressApp,
pgConnection, pgConnection,
shutdown,
} }
} }
@@ -163,6 +176,7 @@ export default async ({
dbConnection?: Connection dbConnection?: Connection
app: Express app: Express
pgConnection: unknown pgConnection: unknown
shutdown: () => Promise<void>
}> => { }> => {
const configModule = loadConfig(rootDirectory) const configModule = loadConfig(rootDirectory)
const featureFlagRouter = featureFlagsLoader(configModule, Logger) const featureFlagRouter = featureFlagsLoader(configModule, Logger)
@@ -197,7 +211,7 @@ export default async ({
featureFlagRouter: asValue(featureFlagRouter), featureFlagRouter: asValue(featureFlagRouter),
}) })
await redisLoader({ container, configModule, logger: Logger }) const { shutdown: redisShutdown } = await redisLoader({ container, configModule, logger: Logger })
const modelsActivity = Logger.activity(`Initializing models${EOL}`) const modelsActivity = Logger.activity(`Initializing models${EOL}`)
track("MODELS_INIT_STARTED") track("MODELS_INIT_STARTED")
@@ -257,7 +271,7 @@ export default async ({
track("MODULES_INIT_STARTED") track("MODULES_INIT_STARTED")
// Move before services init once all modules are migrated and do not rely on core resources anymore // Move before services init once all modules are migrated and do not rely on core resources anymore
await loadMedusaApp({ const { onApplicationShutdown: medusaAppOnApplicationShutdown } = await loadMedusaApp({
configModule, configModule,
container, container,
}) })
@@ -267,7 +281,7 @@ export default async ({
const expActivity = Logger.activity(`Initializing express${EOL}`) const expActivity = Logger.activity(`Initializing express${EOL}`)
track("EXPRESS_INIT_STARTED") track("EXPRESS_INIT_STARTED")
await expressLoader({ app: expressApp, configModule }) const { shutdown: expressShutdown } = await expressLoader({ app: expressApp, configModule })
await passportLoader({ app: expressApp, configModule }) await passportLoader({ app: expressApp, configModule })
const exAct = Logger.success(expActivity, "Express intialized") || {} const exAct = Logger.success(expActivity, "Express intialized") || {}
track("EXPRESS_INIT_COMPLETED", { duration: exAct.duration }) track("EXPRESS_INIT_COMPLETED", { duration: exAct.duration })
@@ -324,11 +338,23 @@ export default async ({
Logger.success(searchActivity, "Indexing event emitted") || {} Logger.success(searchActivity, "Indexing event emitted") || {}
track("SEARCH_ENGINE_INDEXING_COMPLETED", { duration: searchAct.duration }) track("SEARCH_ENGINE_INDEXING_COMPLETED", { duration: searchAct.duration })
async function shutdown() {
await promiseAll([
container.dispose(),
dbConnection?.destroy(),
pgConnection?.context?.destroy(),
redisShutdown(),
expressShutdown(),
medusaAppOnApplicationShutdown(),
])
}
return { return {
configModule, configModule,
container, container,
dbConnection, dbConnection,
app: expressApp, app: expressApp,
pgConnection, pgConnection,
shutdown,
} }
} }
@@ -249,7 +249,6 @@ export async function runModulesLoader({
} }
container: MedusaContainer container: MedusaContainer
}): Promise<void> { }): Promise<void> {
const featureFlagRouter = container.resolve<FlagRouter>("featureFlagRouter")
const injectedDependencies = { const injectedDependencies = {
[ContainerRegistrationKeys.PG_CONNECTION]: container.resolve( [ContainerRegistrationKeys.PG_CONNECTION]: container.resolve(
ContainerRegistrationKeys.PG_CONNECTION ContainerRegistrationKeys.PG_CONNECTION
+13 -5
View File
@@ -15,23 +15,25 @@ async function redisLoader({
container, container,
configModule, configModule,
logger, logger,
}: Options): Promise<void> { }: Options): Promise<{ shutdown: () => Promise<void> }> {
let client!: Redis | FakeRedis
if (configModule.projectConfig.redis_url) { if (configModule.projectConfig.redis_url) {
const redisClient = new Redis(configModule.projectConfig.redis_url, { client = new Redis(configModule.projectConfig.redis_url, {
// Lazy connect to properly handle connection errors // Lazy connect to properly handle connection errors
lazyConnect: true, lazyConnect: true,
...(configModule.projectConfig.redis_options ?? {}), ...(configModule.projectConfig.redis_options ?? {}),
}) })
try { try {
await redisClient.connect() await client.connect()
logger?.info(`Connection to Redis established`) logger?.info(`Connection to Redis established`)
} catch (err) { } catch (err) {
logger?.error(`An error occurred while connecting to Redis:${EOL} ${err}`) logger?.error(`An error occurred while connecting to Redis:${EOL} ${err}`)
} }
container.register({ container.register({
redisClient: asValue(redisClient), redisClient: asValue(client),
}) })
} else { } else {
if (process.env.NODE_ENV === "production") { if (process.env.NODE_ENV === "production") {
@@ -43,12 +45,18 @@ async function redisLoader({
logger.info("Using fake Redis") logger.info("Using fake Redis")
// Economical way of dealing with redis clients // Economical way of dealing with redis clients
const client = new FakeRedis() client = new FakeRedis()
container.register({ container.register({
redisClient: asValue(client), redisClient: asValue(client),
}) })
} }
return {
shutdown: async () => {
client.disconnect()
},
}
} }
export default redisLoader export default redisLoader
@@ -20,7 +20,9 @@ export async function loadInternalModule(
migrationOnly?: boolean, migrationOnly?: boolean,
loaderOnly?: boolean loaderOnly?: boolean
): Promise<{ error?: Error } | void> { ): Promise<{ error?: Error } | void> {
const registrationName = resolution.definition.registrationName const registrationName = !loaderOnly
? resolution.definition.registrationName
: resolution.definition.registrationName + "__loaderOnly"
const { resources } = const { resources } =
resolution.moduleDeclaration as InternalModuleDeclaration resolution.moduleDeclaration as InternalModuleDeclaration
@@ -122,11 +124,8 @@ export async function loadInternalModule(
} }
} }
if (loaderOnly) {
return
}
const moduleService = loadedModule.service const moduleService = loadedModule.service
container.register({ container.register({
[registrationName]: asFunction((cradle) => { [registrationName]: asFunction((cradle) => {
;(moduleService as any).__type = MedusaModuleType ;(moduleService as any).__type = MedusaModuleType
@@ -137,6 +136,12 @@ export async function loadInternalModule(
) )
}).singleton(), }).singleton(),
}) })
if (loaderOnly) {
// The expectation is only to run the loader as standalone, so we do not need to register the service and we need to cleanup all services
const service = container.resolve(registrationName)
await service.__hooks?.onApplicationShutdown()
}
} }
export async function loadModuleMigrations( export async function loadModuleMigrations(
+13 -12
View File
@@ -22,6 +22,7 @@ import {
isObject, isObject,
isString, isString,
ModulesSdkUtils, ModulesSdkUtils,
promiseAll,
} from "@medusajs/utils" } from "@medusajs/utils"
import { asValue } from "awilix" import { asValue } from "awilix"
import { import {
@@ -202,6 +203,7 @@ export type MedusaAppOutput = {
entitiesMap?: Record<string, any> entitiesMap?: Record<string, any>
notFound?: Record<string, Record<string, string>> notFound?: Record<string, Record<string, string>>
runMigrations: RunMigrationFn runMigrations: RunMigrationFn
onApplicationShutdown: () => Promise<void>
} }
export type MedusaAppOptions = { export type MedusaAppOptions = {
@@ -237,19 +239,16 @@ async function MedusaApp_({
migrationOnly = false, migrationOnly = false,
loaderOnly = false, loaderOnly = false,
workerMode = "server", workerMode = "server",
}: MedusaAppOptions & { migrationOnly?: boolean } = {}): Promise<{ }: MedusaAppOptions & {
modules: Record<string, LoadedModule | LoadedModule[]> migrationOnly?: boolean
link: RemoteLink | undefined } = {}): Promise<MedusaAppOutput> {
query: (
query: string | RemoteJoinerQuery | object,
variables?: Record<string, unknown>,
options?: RemoteJoinerOptions
) => Promise<any>
entitiesMap?: Record<string, any>
notFound?: Record<string, Record<string, string>>
runMigrations: RunMigrationFn
}> {
const sharedContainer_ = createMedusaContainer({}, sharedContainer) const sharedContainer_ = createMedusaContainer({}, sharedContainer)
const onApplicationShutdown = async () => {
await promiseAll([
MedusaModule.onApplicationShutdown(),
sharedContainer_.dispose(),
])
}
const modules: MedusaModuleConfig = const modules: MedusaModuleConfig =
modulesConfig ?? modulesConfig ??
@@ -312,6 +311,7 @@ async function MedusaApp_({
if (loaderOnly) { if (loaderOnly) {
return { return {
onApplicationShutdown,
modules: allModules, modules: allModules,
link: undefined, link: undefined,
query: async () => { query: async () => {
@@ -387,6 +387,7 @@ async function MedusaApp_({
} }
return { return {
onApplicationShutdown,
modules: allModules, modules: allModules,
link: remoteLink, link: remoteLink,
query, query,
+15
View File
@@ -15,6 +15,7 @@ import {
} from "@medusajs/types" } from "@medusajs/types"
import { import {
createMedusaContainer, createMedusaContainer,
promiseAll,
simpleHash, simpleHash,
stringifyCircular, stringifyCircular,
} from "@medusajs/utils" } from "@medusajs/utils"
@@ -116,6 +117,20 @@ export class MedusaModule {
} }
} }
} }
public static async onApplicationShutdown(): Promise<void> {
await promiseAll(
[...MedusaModule.instances_.values()].map(instances => {
return Object.values(instances).map((instance: IModuleService) => {
return instance.__hooks?.onApplicationShutdown
?.bind(instance)()
.catch(() => {
// The module should handle this and log it
return void 0
})
})
}).flat()
)
}
public static clearInstances(): void { public static clearInstances(): void {
MedusaModule.instances_.clear() MedusaModule.instances_.clear()
+1
View File
@@ -294,5 +294,6 @@ export interface IModuleService {
*/ */
__hooks?: { __hooks?: {
onApplicationStart?: () => Promise<void> onApplicationStart?: () => Promise<void>
onApplicationShutdown?: () => Promise<void>
} }
} }
@@ -63,6 +63,12 @@ export default async ({
redisPublisher: asValue(redisPublisher), redisPublisher: asValue(redisPublisher),
redisSubscriber: asValue(redisSubscriber), redisSubscriber: asValue(redisSubscriber),
redisQueueName: asValue(queueName), redisQueueName: asValue(queueName),
redisDisconnectHandler: asValue(async () => {
connection.disconnect()
workerConnection.disconnect()
redisPublisher.disconnect()
redisSubscriber.disconnect()
}),
}) })
} }
@@ -5,7 +5,7 @@ import {
TransactionStep, TransactionStep,
} from "@medusajs/orchestration" } from "@medusajs/orchestration"
import { ContainerLike, Context, MedusaContainer } from "@medusajs/types" import { ContainerLike, Context, MedusaContainer } from "@medusajs/types"
import { InjectSharedContext, MedusaContext, isString } from "@medusajs/utils" import { InjectSharedContext, isString, MedusaContext } from "@medusajs/utils"
import { import {
FlowRunOptions, FlowRunOptions,
MedusaWorkflow, MedusaWorkflow,
@@ -76,6 +76,8 @@ export class WorkflowOrchestratorService {
protected redisSubscriber: Redis protected redisSubscriber: Redis
private subscribers: Subscribers = new Map() private subscribers: Subscribers = new Map()
protected redisDistributedTransactionStorage_: RedisDistributedTransactionStorage
constructor({ constructor({
redisDistributedTransactionStorage, redisDistributedTransactionStorage,
redisPublisher, redisPublisher,
@@ -92,6 +94,9 @@ export class WorkflowOrchestratorService {
redisDistributedTransactionStorage.setWorkflowOrchestratorService(this) redisDistributedTransactionStorage.setWorkflowOrchestratorService(this)
DistributedTransaction.setStorage(redisDistributedTransactionStorage) DistributedTransaction.setStorage(redisDistributedTransactionStorage)
this.redisDistributedTransactionStorage_ =
redisDistributedTransactionStorage
this.redisSubscriber.on("message", async (_, message) => { this.redisSubscriber.on("message", async (_, message) => {
const { instanceId, data } = JSON.parse(message) const { instanceId, data } = JSON.parse(message)
@@ -99,6 +104,10 @@ export class WorkflowOrchestratorService {
}) })
} }
async onApplicationShutdown() {
await this.redisDistributedTransactionStorage_.onApplicationShutdown()
}
@InjectSharedContext() @InjectSharedContext()
async run<T = unknown>( async run<T = unknown>(
workflowIdOrWorkflow: string | ReturnWorkflow<any, any, any>, workflowIdOrWorkflow: string | ReturnWorkflow<any, any, any>,
@@ -9,9 +9,9 @@ import {
import { import {
InjectManager, InjectManager,
InjectSharedContext, InjectSharedContext,
isString,
MedusaContext, MedusaContext,
MedusaError, MedusaError,
isString,
} from "@medusajs/utils" } from "@medusajs/utils"
import type { import type {
IWorkflowEngineService, IWorkflowEngineService,
@@ -26,30 +26,41 @@ type InjectedDependencies = {
baseRepository: DAL.RepositoryService baseRepository: DAL.RepositoryService
workflowExecutionService: ModulesSdkTypes.InternalModuleService<any> workflowExecutionService: ModulesSdkTypes.InternalModuleService<any>
workflowOrchestratorService: WorkflowOrchestratorService workflowOrchestratorService: WorkflowOrchestratorService
redisDisconnectHandler: () => Promise<void>
} }
export class WorkflowsModuleService implements IWorkflowEngineService { export class WorkflowsModuleService implements IWorkflowEngineService {
protected baseRepository_: DAL.RepositoryService protected baseRepository_: DAL.RepositoryService
protected workflowExecutionService_: ModulesSdkTypes.InternalModuleService<any> protected workflowExecutionService_: ModulesSdkTypes.InternalModuleService<any>
protected workflowOrchestratorService_: WorkflowOrchestratorService protected workflowOrchestratorService_: WorkflowOrchestratorService
protected redisDisconnectHandler_: () => Promise<void>
constructor( constructor(
{ {
baseRepository, baseRepository,
workflowExecutionService, workflowExecutionService,
workflowOrchestratorService, workflowOrchestratorService,
redisDisconnectHandler,
}: InjectedDependencies, }: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration protected readonly moduleDeclaration: InternalModuleDeclaration
) { ) {
this.baseRepository_ = baseRepository this.baseRepository_ = baseRepository
this.workflowExecutionService_ = workflowExecutionService this.workflowExecutionService_ = workflowExecutionService
this.workflowOrchestratorService_ = workflowOrchestratorService this.workflowOrchestratorService_ = workflowOrchestratorService
this.redisDisconnectHandler_ = redisDisconnectHandler
} }
__joinerConfig(): ModuleJoinerConfig { __joinerConfig(): ModuleJoinerConfig {
return joinerConfig return joinerConfig
} }
__hooks = {
onApplicationShutdown: async () => {
await this.workflowOrchestratorService_.onApplicationShutdown()
await this.redisDisconnectHandler_()
},
}
@InjectManager("baseRepository_") @InjectManager("baseRepository_")
async retrieveWorkflowExecution( async retrieveWorkflowExecution(
idOrObject: idOrObject:
@@ -191,7 +202,7 @@ export class WorkflowsModuleService implements IWorkflowEngineService {
transactionId: string, transactionId: string,
@MedusaContext() context: Context = {} @MedusaContext() context: Context = {}
) { ) {
return this.workflowOrchestratorService_.getRunningTransaction( return await this.workflowOrchestratorService_.getRunningTransaction(
workflowId, workflowId,
transactionId, transactionId,
context context
@@ -211,7 +222,7 @@ export class WorkflowsModuleService implements IWorkflowEngineService {
}, },
@MedusaContext() context: Context = {} @MedusaContext() context: Context = {}
) { ) {
return this.workflowOrchestratorService_.setStepSuccess( return await this.workflowOrchestratorService_.setStepSuccess(
{ {
idempotencyKey, idempotencyKey,
stepResponse, stepResponse,
@@ -234,7 +245,7 @@ export class WorkflowsModuleService implements IWorkflowEngineService {
}, },
@MedusaContext() context: Context = {} @MedusaContext() context: Context = {}
) { ) {
return this.workflowOrchestratorService_.setStepFailure( return await this.workflowOrchestratorService_.setStepFailure(
{ {
idempotencyKey, idempotencyKey,
stepResponse, stepResponse,
@@ -64,6 +64,11 @@ export class RedisDistributedTransactionStorage extends DistributedTransactionSt
) )
} }
async onApplicationShutdown() {
await this.queue.close()
await this.worker.close(true)
}
setWorkflowOrchestratorService(workflowOrchestratorService) { setWorkflowOrchestratorService(workflowOrchestratorService) {
this.workflowOrchestratorService_ = workflowOrchestratorService this.workflowOrchestratorService_ = workflowOrchestratorService
} }