chore(event-bus, workflow-engine): Enable more granualar queues configuration (#14201)
Summary
This PR adds BullMQ queue and worker configuration options to the workflow-engine-redis module, bringing feature parity with the event-bus-redis module. It also introduces per-queue
configuration options for fine-grained control over the three internal queues (main, job, and cleaner).
Key changes:
- Added per-queue BullMQ configuration options (mainQueueOptions, jobQueueOptions, cleanerQueueOptions and their worker counterparts) with shared defaults
- Unified Redis option naming across modules: deprecated url → redisUrl, options → redisOptions (with backward compatibility)
- Moved configuration resolution to the loader and registered options in the DI container
- Added comprehensive JSDoc documentation for all configuration options
- Added unit tests for option merging and queue/worker configuration
Configuration Example
```ts
// Simple configuration - same options for all queues
{
redisUrl: "redis://localhost:6379",
queueOptions: { defaultJobOptions: { removeOnComplete: 1000 } },
workerOptions: { concurrency: 10 }
}
```
```ts
// Advanced configuration - per-queue overrides
{
redisUrl: "redis://localhost:6379",
workerOptions: { concurrency: 10 }, // shared default
jobWorkerOptions: { concurrency: 5 }, // override for scheduled workflows
cleanerWorkerOptions: { concurrency: 1 } // override for cleanup (low priority)
}
```
This commit is contained in:
committed by
GitHub
parent
3e3e6c37bd
commit
144f0f4e2e
+321
@@ -0,0 +1,321 @@
|
||||
import { Logger, ModulesSdkTypes } from "@medusajs/framework/types"
|
||||
import { Queue, Worker } from "bullmq"
|
||||
import Redis from "ioredis"
|
||||
import { RedisDistributedTransactionStorage } from "../workflow-orchestrator-storage"
|
||||
|
||||
jest.mock("bullmq")
|
||||
jest.mock("ioredis")
|
||||
|
||||
const loggerMock = {
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
} as unknown as Logger
|
||||
|
||||
const redisMock = {
|
||||
status: "ready",
|
||||
disconnect: jest.fn(),
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
del: jest.fn(),
|
||||
pipeline: jest.fn(() => ({
|
||||
exec: jest.fn(),
|
||||
})),
|
||||
} as unknown as Redis
|
||||
|
||||
const workflowExecutionServiceMock = {
|
||||
list: jest.fn(),
|
||||
upsert: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
} as unknown as ModulesSdkTypes.IMedusaInternalService<any>
|
||||
|
||||
const baseModuleDeps = {
|
||||
workflowExecutionService: workflowExecutionServiceMock,
|
||||
redisConnection: redisMock,
|
||||
redisWorkerConnection: redisMock,
|
||||
redisQueueName: "medusa-workflows",
|
||||
redisJobQueueName: "medusa-workflows-jobs",
|
||||
logger: loggerMock,
|
||||
isWorkerMode: true,
|
||||
}
|
||||
|
||||
describe("RedisDistributedTransactionStorage", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("constructor - Queue configuration", () => {
|
||||
it("should create queues with default empty options when no options provided", () => {
|
||||
new RedisDistributedTransactionStorage({
|
||||
...baseModuleDeps,
|
||||
redisMainQueueOptions: {},
|
||||
redisMainWorkerOptions: {},
|
||||
redisJobQueueOptions: {},
|
||||
redisJobWorkerOptions: {},
|
||||
redisCleanerQueueOptions: {},
|
||||
redisCleanerWorkerOptions: {},
|
||||
})
|
||||
|
||||
expect(Queue).toHaveBeenCalledWith("medusa-workflows", {
|
||||
connection: redisMock,
|
||||
})
|
||||
|
||||
expect(Queue).toHaveBeenCalledWith("medusa-workflows-jobs", {
|
||||
connection: redisMock,
|
||||
})
|
||||
|
||||
expect(Queue).toHaveBeenCalledWith("workflows-cleaner", {
|
||||
connection: redisMock,
|
||||
})
|
||||
|
||||
expect(Queue).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it("should create main queue with custom options", () => {
|
||||
const mainQueueOptions = {
|
||||
defaultJobOptions: {
|
||||
removeOnComplete: 1000,
|
||||
removeOnFail: 5000,
|
||||
},
|
||||
}
|
||||
|
||||
new RedisDistributedTransactionStorage({
|
||||
...baseModuleDeps,
|
||||
redisMainQueueOptions: mainQueueOptions,
|
||||
redisMainWorkerOptions: {},
|
||||
redisJobQueueOptions: {},
|
||||
redisJobWorkerOptions: {},
|
||||
redisCleanerQueueOptions: {},
|
||||
redisCleanerWorkerOptions: {},
|
||||
})
|
||||
|
||||
expect(Queue).toHaveBeenCalledWith("medusa-workflows", {
|
||||
...mainQueueOptions,
|
||||
connection: redisMock,
|
||||
})
|
||||
})
|
||||
|
||||
it("should create job queue with custom options", () => {
|
||||
const jobQueueOptions = {
|
||||
defaultJobOptions: {
|
||||
attempts: 5,
|
||||
backoff: { type: "exponential", delay: 1000 },
|
||||
},
|
||||
}
|
||||
|
||||
new RedisDistributedTransactionStorage({
|
||||
...baseModuleDeps,
|
||||
redisMainQueueOptions: {},
|
||||
redisMainWorkerOptions: {},
|
||||
redisJobQueueOptions: jobQueueOptions,
|
||||
redisJobWorkerOptions: {},
|
||||
redisCleanerQueueOptions: {},
|
||||
redisCleanerWorkerOptions: {},
|
||||
})
|
||||
|
||||
expect(Queue).toHaveBeenCalledWith("medusa-workflows-jobs", {
|
||||
...jobQueueOptions,
|
||||
connection: redisMock,
|
||||
})
|
||||
})
|
||||
|
||||
it("should create cleaner queue with custom options", () => {
|
||||
const cleanerQueueOptions = {
|
||||
defaultJobOptions: {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
}
|
||||
|
||||
new RedisDistributedTransactionStorage({
|
||||
...baseModuleDeps,
|
||||
redisMainQueueOptions: {},
|
||||
redisMainWorkerOptions: {},
|
||||
redisJobQueueOptions: {},
|
||||
redisJobWorkerOptions: {},
|
||||
redisCleanerQueueOptions: cleanerQueueOptions,
|
||||
redisCleanerWorkerOptions: {},
|
||||
})
|
||||
|
||||
expect(Queue).toHaveBeenCalledWith("workflows-cleaner", {
|
||||
...cleanerQueueOptions,
|
||||
connection: redisMock,
|
||||
})
|
||||
})
|
||||
|
||||
it("should create each queue with different options", () => {
|
||||
const mainQueueOptions = { defaultJobOptions: { removeOnComplete: 100 } }
|
||||
const jobQueueOptions = { defaultJobOptions: { removeOnComplete: 200 } }
|
||||
const cleanerQueueOptions = {
|
||||
defaultJobOptions: { removeOnComplete: 300 },
|
||||
}
|
||||
|
||||
new RedisDistributedTransactionStorage({
|
||||
...baseModuleDeps,
|
||||
redisMainQueueOptions: mainQueueOptions,
|
||||
redisMainWorkerOptions: {},
|
||||
redisJobQueueOptions: jobQueueOptions,
|
||||
redisJobWorkerOptions: {},
|
||||
redisCleanerQueueOptions: cleanerQueueOptions,
|
||||
redisCleanerWorkerOptions: {},
|
||||
})
|
||||
|
||||
expect(Queue).toHaveBeenNthCalledWith(1, "medusa-workflows", {
|
||||
...mainQueueOptions,
|
||||
connection: redisMock,
|
||||
})
|
||||
|
||||
expect(Queue).toHaveBeenNthCalledWith(2, "medusa-workflows-jobs", {
|
||||
...jobQueueOptions,
|
||||
connection: redisMock,
|
||||
})
|
||||
|
||||
expect(Queue).toHaveBeenNthCalledWith(3, "workflows-cleaner", {
|
||||
...cleanerQueueOptions,
|
||||
connection: redisMock,
|
||||
})
|
||||
})
|
||||
|
||||
it("should not create job and cleaner queues when not in worker mode", () => {
|
||||
new RedisDistributedTransactionStorage({
|
||||
...baseModuleDeps,
|
||||
isWorkerMode: false,
|
||||
redisMainQueueOptions: {},
|
||||
redisMainWorkerOptions: {},
|
||||
redisJobQueueOptions: {},
|
||||
redisJobWorkerOptions: {},
|
||||
redisCleanerQueueOptions: {},
|
||||
redisCleanerWorkerOptions: {},
|
||||
})
|
||||
|
||||
expect(Queue).toHaveBeenCalledTimes(1)
|
||||
expect(Queue).toHaveBeenCalledWith("medusa-workflows", {
|
||||
connection: redisMock,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("onApplicationStart - Worker configuration", () => {
|
||||
it("should create workers with custom options", async () => {
|
||||
const mainWorkerOptions = {
|
||||
concurrency: 10,
|
||||
limiter: { max: 100, duration: 1000 },
|
||||
}
|
||||
const jobWorkerOptions = { concurrency: 5 }
|
||||
const cleanerWorkerOptions = { concurrency: 1 }
|
||||
|
||||
const storage = new RedisDistributedTransactionStorage({
|
||||
...baseModuleDeps,
|
||||
redisMainQueueOptions: {},
|
||||
redisMainWorkerOptions: mainWorkerOptions,
|
||||
redisJobQueueOptions: {},
|
||||
redisJobWorkerOptions: jobWorkerOptions,
|
||||
redisCleanerQueueOptions: {},
|
||||
redisCleanerWorkerOptions: cleanerWorkerOptions,
|
||||
})
|
||||
|
||||
const mockQueue = {
|
||||
getRepeatableJobs: jest.fn().mockResolvedValue([]),
|
||||
add: jest.fn().mockResolvedValue({}),
|
||||
}
|
||||
;(storage as any).queue = mockQueue
|
||||
;(storage as any).cleanerQueue_ = mockQueue
|
||||
|
||||
await storage.onApplicationStart()
|
||||
|
||||
expect(Worker).toHaveBeenCalledWith(
|
||||
"medusa-workflows",
|
||||
expect.any(Function),
|
||||
{
|
||||
...mainWorkerOptions,
|
||||
connection: redisMock,
|
||||
}
|
||||
)
|
||||
|
||||
expect(Worker).toHaveBeenCalledWith(
|
||||
"medusa-workflows-jobs",
|
||||
expect.any(Function),
|
||||
{
|
||||
...jobWorkerOptions,
|
||||
connection: redisMock,
|
||||
}
|
||||
)
|
||||
|
||||
expect(Worker).toHaveBeenCalledWith(
|
||||
"workflows-cleaner",
|
||||
expect.any(Function),
|
||||
{
|
||||
...cleanerWorkerOptions,
|
||||
connection: redisMock,
|
||||
}
|
||||
)
|
||||
|
||||
expect(Worker).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it("should create each worker with different concurrency settings", async () => {
|
||||
const storage = new RedisDistributedTransactionStorage({
|
||||
...baseModuleDeps,
|
||||
redisMainQueueOptions: {},
|
||||
redisMainWorkerOptions: { concurrency: 20 },
|
||||
redisJobQueueOptions: {},
|
||||
redisJobWorkerOptions: { concurrency: 10 },
|
||||
redisCleanerQueueOptions: {},
|
||||
redisCleanerWorkerOptions: { concurrency: 1 },
|
||||
})
|
||||
|
||||
const mockQueue = {
|
||||
getRepeatableJobs: jest.fn().mockResolvedValue([]),
|
||||
add: jest.fn().mockResolvedValue({}),
|
||||
}
|
||||
;(storage as any).queue = mockQueue
|
||||
;(storage as any).cleanerQueue_ = mockQueue
|
||||
|
||||
await storage.onApplicationStart()
|
||||
|
||||
expect(Worker).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"medusa-workflows",
|
||||
expect.any(Function),
|
||||
expect.objectContaining({ concurrency: 20 })
|
||||
)
|
||||
|
||||
expect(Worker).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"medusa-workflows-jobs",
|
||||
expect.any(Function),
|
||||
expect.objectContaining({ concurrency: 10 })
|
||||
)
|
||||
|
||||
expect(Worker).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
"workflows-cleaner",
|
||||
expect.any(Function),
|
||||
expect.objectContaining({ concurrency: 1 })
|
||||
)
|
||||
})
|
||||
|
||||
it("should not create workers when not in worker mode", async () => {
|
||||
const storage = new RedisDistributedTransactionStorage({
|
||||
...baseModuleDeps,
|
||||
isWorkerMode: false,
|
||||
redisMainQueueOptions: {},
|
||||
redisMainWorkerOptions: {},
|
||||
redisJobQueueOptions: {},
|
||||
redisJobWorkerOptions: {},
|
||||
redisCleanerQueueOptions: {},
|
||||
redisCleanerWorkerOptions: {},
|
||||
})
|
||||
|
||||
const mockQueue = {
|
||||
getRepeatableJobs: jest.fn().mockResolvedValue([]),
|
||||
}
|
||||
;(storage as any).queue = mockQueue
|
||||
|
||||
await storage.onApplicationStart()
|
||||
|
||||
expect(Worker).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
TransactionStepState,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { WorkflowOrchestratorService } from "@services"
|
||||
import { Queue, RepeatOptions, Worker } from "bullmq"
|
||||
import { Queue, QueueOptions, RepeatOptions, Worker, WorkerOptions } from "bullmq"
|
||||
import Redis from "ioredis"
|
||||
|
||||
enum JobType {
|
||||
@@ -75,6 +75,14 @@ export class RedisDistributedTransactionStorage
|
||||
private cleanerWorker_: Worker
|
||||
private cleanerQueue_?: Queue
|
||||
|
||||
// Per-queue options
|
||||
private mainQueueOptions_: Omit<QueueOptions, "connection">
|
||||
private mainWorkerOptions_: Omit<WorkerOptions, "connection">
|
||||
private jobQueueOptions_: Omit<QueueOptions, "connection">
|
||||
private jobWorkerOptions_: Omit<WorkerOptions, "connection">
|
||||
private cleanerQueueOptions_: Omit<QueueOptions, "connection">
|
||||
private cleanerWorkerOptions_: Omit<WorkerOptions, "connection">
|
||||
|
||||
#isWorkerMode: boolean = false
|
||||
|
||||
constructor({
|
||||
@@ -83,6 +91,12 @@ export class RedisDistributedTransactionStorage
|
||||
redisWorkerConnection,
|
||||
redisQueueName,
|
||||
redisJobQueueName,
|
||||
redisMainQueueOptions,
|
||||
redisMainWorkerOptions,
|
||||
redisJobQueueOptions,
|
||||
redisJobWorkerOptions,
|
||||
redisCleanerQueueOptions,
|
||||
redisCleanerWorkerOptions,
|
||||
logger,
|
||||
isWorkerMode,
|
||||
}: {
|
||||
@@ -91,6 +105,12 @@ export class RedisDistributedTransactionStorage
|
||||
redisWorkerConnection: Redis
|
||||
redisQueueName: string
|
||||
redisJobQueueName: string
|
||||
redisMainQueueOptions: Omit<QueueOptions, "connection">
|
||||
redisMainWorkerOptions: Omit<WorkerOptions, "connection">
|
||||
redisJobQueueOptions: Omit<QueueOptions, "connection">
|
||||
redisJobWorkerOptions: Omit<WorkerOptions, "connection">
|
||||
redisCleanerQueueOptions: Omit<QueueOptions, "connection">
|
||||
redisCleanerWorkerOptions: Omit<WorkerOptions, "connection">
|
||||
logger: Logger
|
||||
isWorkerMode: boolean
|
||||
}) {
|
||||
@@ -101,14 +121,29 @@ export class RedisDistributedTransactionStorage
|
||||
this.cleanerQueueName = "workflows-cleaner"
|
||||
this.queueName = redisQueueName
|
||||
this.jobQueueName = redisJobQueueName
|
||||
this.queue = new Queue(redisQueueName, { connection: this.redisClient })
|
||||
|
||||
// Store per-queue options
|
||||
this.mainQueueOptions_ = redisMainQueueOptions ?? {}
|
||||
this.mainWorkerOptions_ = redisMainWorkerOptions ?? {}
|
||||
this.jobQueueOptions_ = redisJobQueueOptions ?? {}
|
||||
this.jobWorkerOptions_ = redisJobWorkerOptions ?? {}
|
||||
this.cleanerQueueOptions_ = redisCleanerQueueOptions ?? {}
|
||||
this.cleanerWorkerOptions_ = redisCleanerWorkerOptions ?? {}
|
||||
|
||||
// Create queues with their respective options
|
||||
this.queue = new Queue(redisQueueName, {
|
||||
...this.mainQueueOptions_,
|
||||
connection: this.redisClient,
|
||||
})
|
||||
this.jobQueue = isWorkerMode
|
||||
? new Queue(redisJobQueueName, {
|
||||
...this.jobQueueOptions_,
|
||||
connection: this.redisClient,
|
||||
})
|
||||
: undefined
|
||||
this.cleanerQueue_ = isWorkerMode
|
||||
? new Queue(this.cleanerQueueName, {
|
||||
...this.cleanerQueueOptions_,
|
||||
connection: this.redisClient,
|
||||
})
|
||||
: undefined
|
||||
@@ -137,7 +172,17 @@ export class RedisDistributedTransactionStorage
|
||||
JobType.TRANSACTION_TIMEOUT,
|
||||
]
|
||||
|
||||
const workerOptions = {
|
||||
// Per-worker options with their respective configurations
|
||||
const mainWorkerOptions: WorkerOptions = {
|
||||
...this.mainWorkerOptions_,
|
||||
connection: this.redisWorkerConnection,
|
||||
}
|
||||
const jobWorkerOptions: WorkerOptions = {
|
||||
...this.jobWorkerOptions_,
|
||||
connection: this.redisWorkerConnection,
|
||||
}
|
||||
const cleanerWorkerOptions: WorkerOptions = {
|
||||
...this.cleanerWorkerOptions_,
|
||||
connection: this.redisWorkerConnection,
|
||||
}
|
||||
|
||||
@@ -173,7 +218,7 @@ export class RedisDistributedTransactionStorage
|
||||
await this.remove(job.data.jobId)
|
||||
}
|
||||
},
|
||||
workerOptions
|
||||
mainWorkerOptions
|
||||
)
|
||||
|
||||
this.jobWorker = new Worker(
|
||||
@@ -191,7 +236,7 @@ export class RedisDistributedTransactionStorage
|
||||
job.data.schedulerOptions
|
||||
)
|
||||
},
|
||||
workerOptions
|
||||
jobWorkerOptions
|
||||
)
|
||||
|
||||
this.cleanerWorker_ = new Worker(
|
||||
@@ -199,7 +244,7 @@ export class RedisDistributedTransactionStorage
|
||||
async () => {
|
||||
await this.clearExpiredExecutions()
|
||||
},
|
||||
workerOptions
|
||||
cleanerWorkerOptions
|
||||
)
|
||||
|
||||
await this.cleanerQueue_?.add(
|
||||
|
||||
Reference in New Issue
Block a user