fix(workflow-engine-redis): Split the queues and respect worker mode for job executions (#11740)
**What** Currently, the workflow engine redis does not make any distinction between worker modes, when starting as server, the engine listen to the queue which contains everything and try to execute the corresponding workflow which does not exists since job workflows are not loaded in server mode. Now, a dedicated queue is created for jobs and the worker is only started if the instance is not in server mode. In order to clean up the old queue, if the old queue trigger a scheduled job then it gets removed from the queue since it will get re added to the new queue by the new worker instances
This commit is contained in:
1 parent
4f23901fcf
commit
cc8422d3a1
4 files changed
+69
-17
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/workflow-engine-redis": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix(workflow-engine-redis): Split the queues and respect worker mode for job executions
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
import { LoaderOptions } from "@medusajs/framework/types"
|
import {
|
||||||
|
InternalModuleDeclaration,
|
||||||
|
LoaderOptions,
|
||||||
|
} from "@medusajs/framework/types"
|
||||||
import { asValue } from "awilix"
|
import { asValue } from "awilix"
|
||||||
import Redis from "ioredis"
|
import Redis from "ioredis"
|
||||||
import { RedisWorkflowsOptions } from "../types"
|
import { RedisWorkflowsOptions } from "../types"
|
||||||
|
|
||||||
export default async ({
|
export default async (
|
||||||
container,
|
{ container, logger, options, dataLoaderOnly }: LoaderOptions,
|
||||||
logger,
|
moduleDeclaration: InternalModuleDeclaration
|
||||||
options,
|
): Promise<void> => {
|
||||||
dataLoaderOnly,
|
|
||||||
}: LoaderOptions): Promise<void> => {
|
|
||||||
const {
|
const {
|
||||||
url,
|
url,
|
||||||
options: redisOptions,
|
options: redisOptions,
|
||||||
@@ -25,6 +26,7 @@ export default async ({
|
|||||||
const cnnPubSub = pubsub ?? { url, options: redisOptions }
|
const cnnPubSub = pubsub ?? { url, options: redisOptions }
|
||||||
|
|
||||||
const queueName = options?.queueName ?? "medusa-workflows"
|
const queueName = options?.queueName ?? "medusa-workflows"
|
||||||
|
const jobQueueName = options?.jobQueueName ?? "medusa-workflows-jobs"
|
||||||
|
|
||||||
let connection
|
let connection
|
||||||
let redisPublisher
|
let redisPublisher
|
||||||
@@ -59,12 +61,14 @@ export default async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
container.register({
|
container.register({
|
||||||
|
isWorkerMode: asValue(moduleDeclaration.worker_mode !== "server"),
|
||||||
partialLoading: asValue(true),
|
partialLoading: asValue(true),
|
||||||
redisConnection: asValue(connection),
|
redisConnection: asValue(connection),
|
||||||
redisWorkerConnection: asValue(workerConnection),
|
redisWorkerConnection: asValue(workerConnection),
|
||||||
redisPublisher: asValue(redisPublisher),
|
redisPublisher: asValue(redisPublisher),
|
||||||
redisSubscriber: asValue(redisSubscriber),
|
redisSubscriber: asValue(redisSubscriber),
|
||||||
redisQueueName: asValue(queueName),
|
redisQueueName: asValue(queueName),
|
||||||
|
redisJobQueueName: asValue(jobQueueName),
|
||||||
redisDisconnectHandler: asValue(async () => {
|
redisDisconnectHandler: asValue(async () => {
|
||||||
connection.disconnect()
|
connection.disconnect()
|
||||||
workerConnection.disconnect()
|
workerConnection.disconnect()
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ export type RedisWorkflowsOptions = {
|
|||||||
*/
|
*/
|
||||||
queueName?: string
|
queueName?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queue name used for job execution
|
||||||
|
*/
|
||||||
|
jobQueueName?: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redis client options
|
* Redis client options
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -36,37 +36,55 @@ export class RedisDistributedTransactionStorage
|
|||||||
private redisClient: Redis
|
private redisClient: Redis
|
||||||
private redisWorkerConnection: Redis
|
private redisWorkerConnection: Redis
|
||||||
private queueName: string
|
private queueName: string
|
||||||
|
private jobQueueName: string
|
||||||
private queue: Queue
|
private queue: Queue
|
||||||
|
private jobQueue?: Queue
|
||||||
private worker: Worker
|
private worker: Worker
|
||||||
|
private jobWorker?: Worker
|
||||||
|
|
||||||
|
#isWorkerMode: boolean = false
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
workflowExecutionService,
|
workflowExecutionService,
|
||||||
redisConnection,
|
redisConnection,
|
||||||
redisWorkerConnection,
|
redisWorkerConnection,
|
||||||
redisQueueName,
|
redisQueueName,
|
||||||
|
redisJobQueueName,
|
||||||
logger,
|
logger,
|
||||||
|
isWorkerMode,
|
||||||
}: {
|
}: {
|
||||||
workflowExecutionService: ModulesSdkTypes.IMedusaInternalService<any>
|
workflowExecutionService: ModulesSdkTypes.IMedusaInternalService<any>
|
||||||
redisConnection: Redis
|
redisConnection: Redis
|
||||||
redisWorkerConnection: Redis
|
redisWorkerConnection: Redis
|
||||||
redisQueueName: string
|
redisQueueName: string
|
||||||
|
redisJobQueueName: string
|
||||||
logger: Logger
|
logger: Logger
|
||||||
|
isWorkerMode: boolean
|
||||||
}) {
|
}) {
|
||||||
this.workflowExecutionService_ = workflowExecutionService
|
this.workflowExecutionService_ = workflowExecutionService
|
||||||
this.logger_ = logger
|
this.logger_ = logger
|
||||||
this.redisClient = redisConnection
|
this.redisClient = redisConnection
|
||||||
this.redisWorkerConnection = redisWorkerConnection
|
this.redisWorkerConnection = redisWorkerConnection
|
||||||
this.queueName = redisQueueName
|
this.queueName = redisQueueName
|
||||||
|
this.jobQueueName = redisJobQueueName
|
||||||
this.queue = new Queue(redisQueueName, { connection: this.redisClient })
|
this.queue = new Queue(redisQueueName, { connection: this.redisClient })
|
||||||
|
this.jobQueue = isWorkerMode
|
||||||
|
? new Queue(redisJobQueueName, {
|
||||||
|
connection: this.redisClient,
|
||||||
|
})
|
||||||
|
: undefined
|
||||||
|
this.#isWorkerMode = isWorkerMode
|
||||||
}
|
}
|
||||||
|
|
||||||
async onApplicationPrepareShutdown() {
|
async onApplicationPrepareShutdown() {
|
||||||
// Close worker gracefully, i.e. wait for the current jobs to finish
|
// Close worker gracefully, i.e. wait for the current jobs to finish
|
||||||
await this.worker?.close()
|
await this.worker?.close()
|
||||||
|
await this.jobWorker?.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
async onApplicationShutdown() {
|
async onApplicationShutdown() {
|
||||||
await this.queue?.close()
|
await this.queue?.close()
|
||||||
|
await this.jobQueue?.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
async onApplicationStart() {
|
async onApplicationStart() {
|
||||||
@@ -76,6 +94,11 @@ export class RedisDistributedTransactionStorage
|
|||||||
JobType.TRANSACTION_TIMEOUT,
|
JobType.TRANSACTION_TIMEOUT,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const workerOptions = {
|
||||||
|
connection:
|
||||||
|
this.redisWorkerConnection /*, runRetryDelay: 100000 for tests */,
|
||||||
|
}
|
||||||
|
|
||||||
this.worker = new Worker(
|
this.worker = new Worker(
|
||||||
this.queueName,
|
this.queueName,
|
||||||
async (job) => {
|
async (job) => {
|
||||||
@@ -86,20 +109,27 @@ export class RedisDistributedTransactionStorage
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: We might even want a separate worker with different concurrency settings in the future, but for now we keep it simple
|
|
||||||
if (job.name === JobType.SCHEDULE) {
|
if (job.name === JobType.SCHEDULE) {
|
||||||
|
// Remove repeatable job from the old queue since now we have a queue dedicated to scheduled jobs
|
||||||
|
await this.remove(job.data.jobId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
workerOptions
|
||||||
|
)
|
||||||
|
|
||||||
|
if (this.#isWorkerMode) {
|
||||||
|
this.jobWorker = new Worker(
|
||||||
|
this.jobQueueName,
|
||||||
|
async (job) => {
|
||||||
await this.executeScheduledJob(
|
await this.executeScheduledJob(
|
||||||
job.data.jobId,
|
job.data.jobId,
|
||||||
job.data.schedulerOptions
|
job.data.schedulerOptions
|
||||||
)
|
)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
workerOptions
|
||||||
connection:
|
|
||||||
this.redisWorkerConnection /*, runRetryDelay: 100000 for tests */,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setWorkflowOrchestratorService(workflowOrchestratorService) {
|
setWorkflowOrchestratorService(workflowOrchestratorService) {
|
||||||
this.workflowOrchestratorService_ = workflowOrchestratorService
|
this.workflowOrchestratorService_ = workflowOrchestratorService
|
||||||
@@ -369,12 +399,20 @@ export class RedisDistributedTransactionStorage
|
|||||||
step?: TransactionStep
|
step?: TransactionStep
|
||||||
) {
|
) {
|
||||||
const jobId = this.getJobId(type, transaction, step)
|
const jobId = this.getJobId(type, transaction, step)
|
||||||
|
|
||||||
|
if (type === JobType.SCHEDULE) {
|
||||||
|
const job = await this.jobQueue?.getJob(jobId)
|
||||||
|
if (job) {
|
||||||
|
await job.remove()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
const job = await this.queue.getJob(jobId)
|
const job = await this.queue.getJob(jobId)
|
||||||
|
|
||||||
if (job && job.attemptsStarted === 0) {
|
if (job && job.attemptsStarted === 0) {
|
||||||
await job.remove()
|
await job.remove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Scheduler storage methods */
|
/* Scheduler storage methods */
|
||||||
async schedule(
|
async schedule(
|
||||||
@@ -385,7 +423,7 @@ export class RedisDistributedTransactionStorage
|
|||||||
typeof jobDefinition === "string" ? jobDefinition : jobDefinition.jobId
|
typeof jobDefinition === "string" ? jobDefinition : jobDefinition.jobId
|
||||||
|
|
||||||
// If it is the same key (eg. the same workflow name), the old one will get overridden.
|
// If it is the same key (eg. the same workflow name), the old one will get overridden.
|
||||||
await this.queue.add(
|
await this.jobQueue?.add(
|
||||||
JobType.SCHEDULE,
|
JobType.SCHEDULE,
|
||||||
{
|
{
|
||||||
jobId,
|
jobId,
|
||||||
@@ -410,13 +448,13 @@ export class RedisDistributedTransactionStorage
|
|||||||
}
|
}
|
||||||
|
|
||||||
async remove(jobId: string): Promise<void> {
|
async remove(jobId: string): Promise<void> {
|
||||||
await this.queue.removeRepeatableByKey(`${JobType.SCHEDULE}_${jobId}`)
|
await this.jobQueue?.removeRepeatableByKey(`${JobType.SCHEDULE}_${jobId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeAll(): Promise<void> {
|
async removeAll(): Promise<void> {
|
||||||
const repeatableJobs = await this.queue.getRepeatableJobs()
|
const repeatableJobs = (await this.jobQueue?.getRepeatableJobs()) ?? []
|
||||||
await promiseAll(
|
await promiseAll(
|
||||||
repeatableJobs.map((job) => this.queue.removeRepeatableByKey(job.key))
|
repeatableJobs.map((job) => this.jobQueue?.removeRepeatableByKey(job.key))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user