@open-mercato/queue 0.6.7 → 0.6.8-develop.6875.1.871a4afc94

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/worker/runner.ts"],
4
- "sourcesContent": ["import { createQueue } from '../factory'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { Queue, JobHandler, AsyncQueueOptions, QueueStrategyType } from '../types'\n\nconst logger = createLogger('queue').child({ component: 'worker' })\n\n/**\n * Options for running a queue worker.\n */\nexport type WorkerRunnerOptions<T = unknown> = {\n /** Name of the queue to process */\n queueName: string\n /** Handler function to process each job */\n handler: JobHandler<T>\n /** Redis connection options (only used for async strategy) */\n connection?: AsyncQueueOptions['connection']\n /** Number of concurrent jobs to process */\n concurrency?: number\n /** Whether to set up graceful shutdown handlers */\n gracefulShutdown?: boolean\n /** If true, don't block - return immediately after starting processing (for multi-queue mode) */\n background?: boolean\n /** Queue strategy to use. Defaults to QUEUE_STRATEGY env var or 'local' */\n strategy?: QueueStrategyType\n}\n\nconst managedQueues = new Set<Queue<unknown>>()\nconst managedShutdownHooks = new Set<() => Promise<void> | void>()\nlet shutdownHandlersRegistered = false\nlet shutdownInProgress = false\n\nfunction unregisterShutdownHandlers(sigtermHandler: () => void, sigintHandler: () => void): void {\n process.off('SIGTERM', sigtermHandler)\n process.off('SIGINT', sigintHandler)\n shutdownHandlersRegistered = false\n}\n\nfunction registerShutdownHandlers(): void {\n if (shutdownHandlersRegistered) return\n\n const shutdown = async (signal: string) => {\n if (shutdownInProgress) return\n shutdownInProgress = true\n\n logger.info('Received signal, shutting down gracefully', { signal })\n\n let hasError = false\n for (const queue of managedQueues) {\n try {\n await queue.close()\n } catch (error) {\n hasError = true\n logger.error('Error during shutdown', { err: error })\n }\n }\n\n managedQueues.clear()\n for (const hook of managedShutdownHooks) {\n try {\n await hook()\n } catch (error) {\n hasError = true\n logger.error('Error during shutdown hook', { err: error })\n }\n }\n managedShutdownHooks.clear()\n unregisterShutdownHandlers(sigtermHandler, sigintHandler)\n shutdownInProgress = false\n\n if (!hasError) {\n logger.info('Worker closed successfully')\n }\n\n process.exit(hasError ? 1 : 0)\n }\n\n const sigtermHandler = () => {\n void shutdown('SIGTERM')\n }\n\n const sigintHandler = () => {\n void shutdown('SIGINT')\n }\n\n process.on('SIGTERM', sigtermHandler)\n process.on('SIGINT', sigintHandler)\n shutdownHandlersRegistered = true\n}\n\n/**\n * Register a process-local service that must stop before a worker exits.\n * The returned callback removes the hook when the service is stopped early.\n */\nexport function registerWorkerShutdownHook(hook: () => Promise<void> | void): () => void {\n managedShutdownHooks.add(hook)\n return () => managedShutdownHooks.delete(hook)\n}\n\n/**\n * Runs a queue worker that processes jobs continuously.\n *\n * This function:\n * 1. Creates an async queue instance\n * 2. Starts a BullMQ worker\n * 3. Sets up graceful shutdown on SIGTERM/SIGINT\n * 4. Keeps the process running until shutdown\n *\n * @template T - The job payload type\n * @param options - Worker configuration\n *\n * @example\n * ```typescript\n * import { runWorker } from '@open-mercato/queue/worker'\n *\n * await runWorker({\n * queueName: 'events',\n * handler: async (job, ctx) => {\n * console.log(`Processing ${ctx.jobId}:`, job.payload)\n * },\n * connection: { url: process.env.REDIS_URL },\n * concurrency: 5,\n * })\n * ```\n */\nexport async function runWorker<T = unknown>(\n options: WorkerRunnerOptions<T>\n): Promise<void> {\n const {\n queueName,\n handler,\n connection,\n concurrency = 1,\n gracefulShutdown = true,\n background = false,\n strategy: strategyOption,\n } = options\n\n // Determine queue strategy from option, env var, or default to 'local'\n const strategy: QueueStrategyType = strategyOption\n ?? (process.env.QUEUE_STRATEGY === 'async' ? 'async' : 'local')\n\n logger.info('Starting worker for queue', { queueName, strategy })\n\n const queue = createQueue<T>(queueName, strategy, {\n connection,\n concurrency,\n })\n\n // Set up graceful shutdown\n if (gracefulShutdown) {\n managedQueues.add(queue as Queue<unknown>)\n registerShutdownHandlers()\n }\n\n // Start processing\n await queue.process(handler)\n\n logger.info('Worker running', { concurrency })\n\n if (background) {\n // Return immediately for multi-queue mode\n return\n }\n\n logger.info('Press Ctrl+C to stop')\n\n // Keep the process alive (single-queue mode)\n await new Promise(() => {\n // This promise never resolves, keeping the worker running\n })\n}\n\n/**\n * Creates a worker handler that routes jobs to specific handlers based on job type.\n *\n * @template T - Base job payload type (must include a 'type' field)\n * @param handlers - Map of job types to their handlers\n *\n * @example\n * ```typescript\n * const handler = createRoutedHandler({\n * 'user.created': async (job) => { ... },\n * 'order.placed': async (job) => { ... },\n * })\n *\n * await runWorker({ queueName: 'events', handler })\n * ```\n */\nexport function createRoutedHandler<T extends { type: string }>(\n handlers: Record<string, JobHandler<T>>\n): JobHandler<T> {\n return async (job, ctx) => {\n const type = job.payload.type\n const handler = handlers[type]\n\n if (!handler) {\n logger.warn('No handler registered for job type', { type })\n return\n }\n\n await handler(job, ctx)\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,mBAAmB;AAC5B,SAAS,oBAAoB;AAG7B,MAAM,SAAS,aAAa,OAAO,EAAE,MAAM,EAAE,WAAW,SAAS,CAAC;AAsBlE,MAAM,gBAAgB,oBAAI,IAAoB;AAC9C,MAAM,uBAAuB,oBAAI,IAAgC;AACjE,IAAI,6BAA6B;AACjC,IAAI,qBAAqB;AAEzB,SAAS,2BAA2B,gBAA4B,eAAiC;AAC/F,UAAQ,IAAI,WAAW,cAAc;AACrC,UAAQ,IAAI,UAAU,aAAa;AACnC,+BAA6B;AAC/B;AAEA,SAAS,2BAAiC;AACxC,MAAI,2BAA4B;AAEhC,QAAM,WAAW,OAAO,WAAmB;AACzC,QAAI,mBAAoB;AACxB,yBAAqB;AAErB,WAAO,KAAK,6CAA6C,EAAE,OAAO,CAAC;AAEnE,QAAI,WAAW;AACf,eAAW,SAAS,eAAe;AACjC,UAAI;AACF,cAAM,MAAM,MAAM;AAAA,MACpB,SAAS,OAAO;AACd,mBAAW;AACX,eAAO,MAAM,yBAAyB,EAAE,KAAK,MAAM,CAAC;AAAA,MACtD;AAAA,IACF;AAEA,kBAAc,MAAM;AACpB,eAAW,QAAQ,sBAAsB;AACvC,UAAI;AACF,cAAM,KAAK;AAAA,MACb,SAAS,OAAO;AACd,mBAAW;AACX,eAAO,MAAM,8BAA8B,EAAE,KAAK,MAAM,CAAC;AAAA,MAC3D;AAAA,IACF;AACA,yBAAqB,MAAM;AAC3B,+BAA2B,gBAAgB,aAAa;AACxD,yBAAqB;AAErB,QAAI,CAAC,UAAU;AACb,aAAO,KAAK,4BAA4B;AAAA,IAC1C;AAEA,YAAQ,KAAK,WAAW,IAAI,CAAC;AAAA,EAC/B;AAEA,QAAM,iBAAiB,MAAM;AAC3B,SAAK,SAAS,SAAS;AAAA,EACzB;AAEA,QAAM,gBAAgB,MAAM;AAC1B,SAAK,SAAS,QAAQ;AAAA,EACxB;AAEA,UAAQ,GAAG,WAAW,cAAc;AACpC,UAAQ,GAAG,UAAU,aAAa;AAClC,+BAA6B;AAC/B;AAMO,SAAS,2BAA2B,MAA8C;AACvF,uBAAqB,IAAI,IAAI;AAC7B,SAAO,MAAM,qBAAqB,OAAO,IAAI;AAC/C;AA4BA,eAAsB,UACpB,SACe;AACf,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc;AAAA,IACd,mBAAmB;AAAA,IACnB,aAAa;AAAA,IACb,UAAU;AAAA,EACZ,IAAI;AAGJ,QAAM,WAA8B,mBAC9B,QAAQ,IAAI,mBAAmB,UAAU,UAAU;AAEzD,SAAO,KAAK,6BAA6B,EAAE,WAAW,SAAS,CAAC;AAEhE,QAAM,QAAQ,YAAe,WAAW,UAAU;AAAA,IAChD;AAAA,IACA;AAAA,EACF,CAAC;AAGD,MAAI,kBAAkB;AACpB,kBAAc,IAAI,KAAuB;AACzC,6BAAyB;AAAA,EAC3B;AAGA,QAAM,MAAM,QAAQ,OAAO;AAE3B,SAAO,KAAK,kBAAkB,EAAE,YAAY,CAAC;AAE7C,MAAI,YAAY;AAEd;AAAA,EACF;AAEA,SAAO,KAAK,sBAAsB;AAGlC,QAAM,IAAI,QAAQ,MAAM;AAAA,EAExB,CAAC;AACH;AAkBO,SAAS,oBACd,UACe;AACf,SAAO,OAAO,KAAK,QAAQ;AACzB,UAAM,OAAO,IAAI,QAAQ;AACzB,UAAM,UAAU,SAAS,IAAI;AAE7B,QAAI,CAAC,SAAS;AACZ,aAAO,KAAK,sCAAsC,EAAE,KAAK,CAAC;AAC1D;AAAA,IACF;AAEA,UAAM,QAAQ,KAAK,GAAG;AAAA,EACxB;AACF;",
4
+ "sourcesContent": ["import { createQueue } from '../factory'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport {\n getTelemetryRuntime,\n isTelemetryBackendEnabled,\n} from '@open-mercato/shared/lib/telemetry/runtime'\nimport type { Queue, JobHandler, AsyncQueueOptions, QueueStrategyType } from '../types'\n\nconst logger = createLogger('queue').child({ component: 'worker' })\n\n/**\n * Options for running a queue worker.\n */\nexport type WorkerRunnerOptions<T = unknown> = {\n /** Name of the queue to process */\n queueName: string\n /** Handler function to process each job */\n handler: JobHandler<T>\n /** Redis connection options (only used for async strategy) */\n connection?: AsyncQueueOptions['connection']\n /** Number of concurrent jobs to process */\n concurrency?: number\n /** How long a job lock is held before the job counts as stalled, in ms. */\n lockDuration?: number\n /** Number of stalled-job recoveries BullMQ permits before failing a job. */\n maxStalledCount?: number\n /** Whether to set up graceful shutdown handlers */\n gracefulShutdown?: boolean\n /** If true, don't block - return immediately after starting processing (for multi-queue mode) */\n background?: boolean\n /** Queue strategy to use. Defaults to QUEUE_STRATEGY env var or 'local' */\n strategy?: QueueStrategyType\n}\n\nconst managedQueues = new Set<Queue<unknown>>()\nconst managedShutdownHooks = new Set<() => Promise<void> | void>()\nlet shutdownHandlersRegistered = false\nlet shutdownInProgress = false\n\nfunction unregisterShutdownHandlers(sigtermHandler: () => void, sigintHandler: () => void): void {\n process.off('SIGTERM', sigtermHandler)\n process.off('SIGINT', sigintHandler)\n shutdownHandlersRegistered = false\n}\n\nfunction registerShutdownHandlers(): void {\n if (shutdownHandlersRegistered) return\n\n const shutdown = async (signal: string) => {\n if (shutdownInProgress) return\n shutdownInProgress = true\n\n logger.info('Received signal, shutting down gracefully', { signal })\n\n let hasError = false\n for (const queue of managedQueues) {\n try {\n await queue.close()\n } catch (error) {\n hasError = true\n logger.error('Error during shutdown', { err: error })\n }\n }\n\n managedQueues.clear()\n for (const hook of managedShutdownHooks) {\n try {\n await hook()\n } catch (error) {\n hasError = true\n logger.error('Error during shutdown hook', { err: error })\n }\n }\n managedShutdownHooks.clear()\n unregisterShutdownHandlers(sigtermHandler, sigintHandler)\n shutdownInProgress = false\n\n // Flush buffered spans/logs before the process dies. A worker never returns\n // from run(), so bin.ts's post-run shutdownTelemetry() is unreachable on this\n // path \u2014 without this, the BatchSpanProcessor's ~5s tail is dropped on every\n // restart/redeploy. Idempotent and a no-op when telemetry is off; a flush\n // failure must not turn a clean shutdown into a failed one.\n try {\n await getTelemetryRuntime()?.shutdown()\n } catch (error) {\n logger.error('Error flushing telemetry during shutdown', { err: error })\n }\n\n if (!hasError) {\n logger.info('Worker closed successfully')\n }\n\n process.exit(hasError ? 1 : 0)\n }\n\n const sigtermHandler = () => {\n void shutdown('SIGTERM')\n }\n\n const sigintHandler = () => {\n void shutdown('SIGINT')\n }\n\n process.on('SIGTERM', sigtermHandler)\n process.on('SIGINT', sigintHandler)\n shutdownHandlersRegistered = true\n}\n\n/**\n * Register a process-local service that must stop before a worker exits.\n * The returned callback removes the hook when the service is stopped early.\n */\nexport function registerWorkerShutdownHook(hook: () => Promise<void> | void): () => void {\n managedShutdownHooks.add(hook)\n return () => managedShutdownHooks.delete(hook)\n}\n\n/**\n * Runs a queue worker that processes jobs continuously.\n *\n * This function:\n * 1. Creates an async queue instance\n * 2. Starts a BullMQ worker\n * 3. Sets up graceful shutdown on SIGTERM/SIGINT\n * 4. Keeps the process running until shutdown\n *\n * @template T - The job payload type\n * @param options - Worker configuration\n *\n * @example\n * ```typescript\n * import { runWorker } from '@open-mercato/queue/worker'\n *\n * await runWorker({\n * queueName: 'events',\n * handler: async (job, ctx) => {\n * console.log(`Processing ${ctx.jobId}:`, job.payload)\n * },\n * connection: { url: process.env.REDIS_URL },\n * concurrency: 5,\n * })\n * ```\n */\nexport async function runWorker<T = unknown>(\n options: WorkerRunnerOptions<T>\n): Promise<void> {\n const {\n queueName,\n handler,\n connection,\n concurrency = 1,\n lockDuration,\n maxStalledCount,\n gracefulShutdown = true,\n background = false,\n strategy: strategyOption,\n } = options\n\n // Worker processes don't run Next's instrumentation hook, so initialize\n // telemetry here \u2014 this is the single bootstrap every standalone worker passes\n // through. Import the telemetry package only for an explicit enabled backend;\n // with the default/unset backend the worker never evaluates the package.\n if (!getTelemetryRuntime() && isTelemetryBackendEnabled()) {\n const { initTelemetry } = await import('@open-mercato/telemetry')\n await initTelemetry()\n }\n\n // Determine queue strategy from option, env var, or default to 'local'\n const strategy: QueueStrategyType = strategyOption\n ?? (process.env.QUEUE_STRATEGY === 'async' ? 'async' : 'local')\n\n logger.info('Starting worker for queue', { queueName, strategy })\n\n const queue = createQueue<T>(queueName, strategy, {\n connection,\n concurrency,\n lockDuration,\n maxStalledCount,\n })\n\n // Set up graceful shutdown\n if (gracefulShutdown) {\n managedQueues.add(queue as Queue<unknown>)\n registerShutdownHandlers()\n }\n\n // Start processing\n await queue.process(handler)\n\n logger.info('Worker running', { concurrency })\n\n if (background) {\n // Return immediately for multi-queue mode\n return\n }\n\n logger.info('Press Ctrl+C to stop')\n\n // Keep the process alive (single-queue mode)\n await new Promise(() => {\n // This promise never resolves, keeping the worker running\n })\n}\n\n/**\n * Creates a worker handler that routes jobs to specific handlers based on job type.\n *\n * @template T - Base job payload type (must include a 'type' field)\n * @param handlers - Map of job types to their handlers\n *\n * @example\n * ```typescript\n * const handler = createRoutedHandler({\n * 'user.created': async (job) => { ... },\n * 'order.placed': async (job) => { ... },\n * })\n *\n * await runWorker({ queueName: 'events', handler })\n * ```\n */\nexport function createRoutedHandler<T extends { type: string }>(\n handlers: Record<string, JobHandler<T>>\n): JobHandler<T> {\n return async (job, ctx) => {\n const type = job.payload.type\n const handler = handlers[type]\n\n if (!handler) {\n logger.warn('No handler registered for job type', { type })\n return\n }\n\n await handler(job, ctx)\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/jest.config.cjs CHANGED
@@ -26,5 +26,6 @@ module.exports = {
26
26
  passWithNoTests: true,
27
27
  moduleNameMapper: {
28
28
  '^@open-mercato/shared/(.*)$': '<rootDir>/../shared/src/$1',
29
+ '^@open-mercato/telemetry$': '<rootDir>/../telemetry/src/index.ts',
29
30
  },
30
31
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/queue",
3
- "version": "0.6.7",
3
+ "version": "0.6.8-develop.6875.1.871a4afc94",
4
4
  "license": "MIT",
5
5
  "description": "Multi-strategy job queue with local and BullMQ support",
6
6
  "type": "module",
@@ -34,29 +34,35 @@
34
34
  }
35
35
  },
36
36
  "peerDependencies": {
37
- "bullmq": "^5.0.0"
37
+ "bullmq": "^5.0.0 || ^6.0.0",
38
+ "bullmq-otel": "^1.3.0"
38
39
  },
39
40
  "peerDependenciesMeta": {
40
41
  "bullmq": {
41
42
  "optional": true
43
+ },
44
+ "bullmq-otel": {
45
+ "optional": true
42
46
  }
43
47
  },
44
48
  "devDependencies": {
45
49
  "@types/jest": "^30.0.0",
46
- "@types/node": "^26.0.1",
50
+ "@types/node": "^26.1.2",
47
51
  "jest": "^30.4.2",
48
- "ts-jest": "^29.4.11",
52
+ "ts-jest": "^29.4.12",
49
53
  "typescript": "7.0.2"
50
54
  },
51
55
  "publishConfig": {
52
56
  "access": "public"
53
57
  },
54
58
  "dependencies": {
55
- "@open-mercato/shared": "0.6.7"
59
+ "@open-mercato/shared": "0.6.8-develop.6875.1.871a4afc94",
60
+ "@open-mercato/telemetry": "0.6.8-develop.6875.1.871a4afc94"
56
61
  },
57
62
  "repository": {
58
63
  "type": "git",
59
64
  "url": "https://github.com/open-mercato/open-mercato",
60
65
  "directory": "packages/queue"
61
- }
66
+ },
67
+ "stableVersion": "0.6.7"
62
68
  }
@@ -18,6 +18,7 @@ const workerOn = jest.fn()
18
18
 
19
19
  jest.mock('@open-mercato/shared/lib/redis/connection', () => ({
20
20
  getRedisUrlOrThrow: jest.fn(),
21
+ parseRedisUrl: jest.requireActual('@open-mercato/shared/lib/redis/connection').parseRedisUrl,
21
22
  }))
22
23
 
23
24
  jest.mock('bullmq', () => {
@@ -60,7 +61,7 @@ describe('Queue - async strategy', () => {
60
61
  getRedisUrlOrThrowMock.mockReturnValue('rediss://default:secret@example.com:6380/1')
61
62
  })
62
63
 
63
- it('passes the full Redis URL to BullMQ when using env-based async config', async () => {
64
+ it('passes parsed Redis connection fields to BullMQ for env-based async config', async () => {
64
65
  const queue = createQueue<{ value: number }>('test-queue', 'async', {
65
66
  concurrency: 3,
66
67
  })
@@ -69,19 +70,35 @@ describe('Queue - async strategy', () => {
69
70
  await queue.process(async () => {})
70
71
 
71
72
  expect(queueCtor).toHaveBeenCalledWith('test-queue', {
72
- connection: { url: 'rediss://default:secret@example.com:6380/1' },
73
+ connection: {
74
+ host: 'example.com',
75
+ port: 6380,
76
+ username: 'default',
77
+ password: 'secret',
78
+ db: 1,
79
+ tls: {},
80
+ family: undefined,
81
+ },
73
82
  })
74
83
  expect(workerCtor).toHaveBeenCalledWith(
75
84
  'test-queue',
76
85
  expect.any(Function),
77
86
  {
78
- connection: { url: 'rediss://default:secret@example.com:6380/1' },
87
+ connection: {
88
+ host: 'example.com',
89
+ port: 6380,
90
+ username: 'default',
91
+ password: 'secret',
92
+ db: 1,
93
+ tls: {},
94
+ family: undefined,
95
+ },
79
96
  concurrency: 3,
80
97
  },
81
98
  )
82
99
  })
83
100
 
84
- it('preserves an explicit Redis URL without converting it to host/port fields', async () => {
101
+ it('preserves URL connection semantics when converting to BullMQ fields', async () => {
85
102
  const queue = createQueue<{ value: number }>('test-queue', 'async', {
86
103
  connection: {
87
104
  url: 'rediss://user:secret@example.com:6380/4?family=6',
@@ -91,7 +108,15 @@ describe('Queue - async strategy', () => {
91
108
  await queue.enqueue({ value: 42 })
92
109
 
93
110
  expect(queueCtor).toHaveBeenCalledWith('test-queue', {
94
- connection: { url: 'rediss://user:secret@example.com:6380/4?family=6' },
111
+ connection: {
112
+ host: 'example.com',
113
+ port: 6380,
114
+ username: 'user',
115
+ password: 'secret',
116
+ db: 4,
117
+ tls: {},
118
+ family: 6,
119
+ },
95
120
  })
96
121
  })
97
122
 
@@ -114,6 +139,38 @@ describe('Queue - async strategy', () => {
114
139
  await queue.close()
115
140
  })
116
141
 
142
+ it('threads queue retry, lock-duration and stalled-job options to BullMQ', async () => {
143
+ const queue = createQueue<{ value: number }>('test-queue', 'async', {
144
+ attempts: 5,
145
+ lockDuration: 120_000,
146
+ maxStalledCount: 10,
147
+ })
148
+
149
+ await queue.enqueue({ value: 42 })
150
+ await queue.process(async () => {})
151
+
152
+ expect(queueAdd).toHaveBeenCalledWith(
153
+ expect.any(String),
154
+ expect.objectContaining({ payload: { value: 42 } }),
155
+ expect.objectContaining({ attempts: 5 }),
156
+ )
157
+ expect(workerCtor).toHaveBeenCalledWith(
158
+ 'test-queue',
159
+ expect.any(Function),
160
+ expect.objectContaining({ lockDuration: 120_000, maxStalledCount: 10 }),
161
+ )
162
+ })
163
+
164
+ it('leaves BullMQ on its own lock and stall defaults when the options are unset', async () => {
165
+ const queue = createQueue<{ value: number }>('test-queue', 'async', {})
166
+
167
+ await queue.process(async () => {})
168
+
169
+ const workerOptions = workerCtor.mock.calls[0]?.[2] as Record<string, unknown>
170
+ expect(workerOptions).not.toHaveProperty('lockDuration')
171
+ expect(workerOptions).not.toHaveProperty('maxStalledCount')
172
+ })
173
+
117
174
  it('removeQueuedJobsByScope removes only queued jobs matching tenant scope', async () => {
118
175
  const removeMatching = jest.fn(async () => {})
119
176
  const removeAutoIndex = jest.fn(async () => {})
@@ -0,0 +1,123 @@
1
+ // An OTLP backend must be active for the async strategy to delegate tracing to
2
+ // bullmq-otel. Set before any module reads the (memoized) telemetry env, and
3
+ // restored afterwards — process.env is shared across test files in the same
4
+ // jest worker, and a leaked 'otlp' backend breaks sibling telemetry tests.
5
+ const originalTelemetryBackend = process.env.TELEMETRY_BACKEND
6
+ process.env.TELEMETRY_BACKEND = 'otlp'
7
+
8
+ afterAll(() => {
9
+ if (originalTelemetryBackend === undefined) delete process.env.TELEMETRY_BACKEND
10
+ else process.env.TELEMETRY_BACKEND = originalTelemetryBackend
11
+ })
12
+
13
+ import { createQueue } from '../factory'
14
+ import { getRedisUrlOrThrow } from '@open-mercato/shared/lib/redis/connection'
15
+ import {
16
+ registerTelemetryRuntime,
17
+ resetTelemetryRuntime,
18
+ } from '@open-mercato/shared/lib/telemetry/runtime'
19
+
20
+ const queueCtor = jest.fn()
21
+ const workerCtor = jest.fn()
22
+ const queueAdd = jest.fn(async () => ({ id: 'bull-job-id' }))
23
+
24
+ jest.mock('@open-mercato/shared/lib/redis/connection', () => ({
25
+ getRedisUrlOrThrow: jest.fn(),
26
+ parseRedisUrl: jest.requireActual('@open-mercato/shared/lib/redis/connection').parseRedisUrl,
27
+ }))
28
+
29
+ jest.mock('bullmq', () => {
30
+ class MockQueue<T> {
31
+ constructor(name: string, opts: unknown) {
32
+ queueCtor(name, opts)
33
+ }
34
+ add = queueAdd as unknown as (name: string, data: T, opts?: unknown) => Promise<{ id?: string }>
35
+ close = jest.fn(async () => {})
36
+ obliterate = jest.fn(async () => {})
37
+ getJobCounts = jest.fn(async () => ({ waiting: 0, active: 0, completed: 0, failed: 0 }))
38
+ }
39
+ class MockWorker<T> {
40
+ constructor(
41
+ name: string,
42
+ _processor: (job: { id?: string; data: T; attemptsMade: number }) => Promise<void>,
43
+ opts: unknown,
44
+ ) {
45
+ workerCtor(name, _processor, opts)
46
+ }
47
+ on = jest.fn()
48
+ close = jest.fn(async () => {})
49
+ }
50
+ return { Queue: MockQueue, Worker: MockWorker }
51
+ })
52
+
53
+ class MockBullMQOtel {
54
+ constructor(public readonly tracerName: string) {}
55
+ }
56
+ jest.mock('bullmq-otel', () => ({ BullMQOtel: MockBullMQOtel }))
57
+
58
+ describe('Queue - async strategy telemetry wiring', () => {
59
+ beforeEach(() => {
60
+ jest.clearAllMocks()
61
+ ;(getRedisUrlOrThrow as jest.MockedFunction<typeof getRedisUrlOrThrow>).mockReturnValue(
62
+ 'rediss://default:secret@example.com:6380/1',
63
+ )
64
+ registerTelemetryRuntime({
65
+ canUseGlobalTracePropagation: () => true,
66
+ captureTraceContext: () => ({}),
67
+ continueTrace: (_carrier, _name, fn) => fn(),
68
+ recordHttpDuration: () => {},
69
+ reportError: () => {},
70
+ shutdown: async () => {},
71
+ })
72
+ })
73
+
74
+ afterEach(() => {
75
+ resetTelemetryRuntime()
76
+ })
77
+
78
+ it('wires bullmq-otel into BOTH the queue and worker when they resolve concurrently', async () => {
79
+ const queue = createQueue<{ value: number }>('trace-queue', 'async', { concurrency: 3 })
80
+
81
+ // Resolve enqueue (Queue) and process (Worker) concurrently: both hit the
82
+ // shared telemetry resolution at once. The memoized in-flight promise must
83
+ // hand both the SAME bullmq-otel instance — never one with, one without.
84
+ await Promise.all([queue.enqueue({ value: 1 }), queue.process(async () => {})])
85
+
86
+ const queueOpts = queueCtor.mock.calls[0]?.[1] as { telemetry?: unknown }
87
+ const workerOpts = workerCtor.mock.calls[0]?.[2] as { telemetry?: unknown }
88
+ expect(queueOpts.telemetry).toBeInstanceOf(MockBullMQOtel)
89
+ expect(workerOpts.telemetry).toBeInstanceOf(MockBullMQOtel)
90
+ expect(queueOpts.telemetry).toBe(workerOpts.telemetry)
91
+ })
92
+
93
+ it('omits the metadata._trace carrier when bullmq-otel owns propagation', async () => {
94
+ const queue = createQueue<{ value: number }>('trace-queue', 'async')
95
+
96
+ await queue.enqueue({ value: 42 })
97
+
98
+ const jobData = queueAdd.mock.calls[0]?.[1] as Record<string, unknown>
99
+ expect(jobData).not.toHaveProperty('metadata')
100
+ })
101
+
102
+ it('uses the dedicated carrier when global propagation is not explicitly trusted', async () => {
103
+ resetTelemetryRuntime()
104
+ registerTelemetryRuntime({
105
+ canUseGlobalTracePropagation: () => false,
106
+ captureTraceContext: () => ({ traceparent: 'secure-carrier' }),
107
+ continueTrace: (_carrier, _name, fn) => fn(),
108
+ recordHttpDuration: () => {},
109
+ reportError: () => {},
110
+ shutdown: async () => {},
111
+ })
112
+ const queue = createQueue<{ value: number }>('secure-trace-queue', 'async')
113
+
114
+ await queue.enqueue({ value: 7 })
115
+
116
+ const queueOpts = queueCtor.mock.calls[0]?.[1] as { telemetry?: unknown }
117
+ const jobData = queueAdd.mock.calls[0]?.[1] as {
118
+ metadata?: { _trace?: { traceparent?: string } }
119
+ }
120
+ expect(queueOpts.telemetry).toBeUndefined()
121
+ expect(jobData.metadata?._trace?.traceparent).toBe('secure-carrier')
122
+ })
123
+ })
@@ -1,8 +1,9 @@
1
1
  import { resolveQueueStrategy, createModuleQueue } from '../factory'
2
- import { getRedisUrlOrThrow } from '@open-mercato/shared/lib/redis/connection'
2
+ import { getRedisUrlOrThrow, parseRedisUrl } from '@open-mercato/shared/lib/redis/connection'
3
3
 
4
4
  jest.mock('@open-mercato/shared/lib/redis/connection', () => ({
5
5
  getRedisUrlOrThrow: jest.fn(),
6
+ parseRedisUrl: jest.fn(),
6
7
  }))
7
8
 
8
9
  jest.mock('bullmq', () => {
@@ -58,10 +59,12 @@ describe('resolveQueueStrategy', () => {
58
59
  describe('createModuleQueue', () => {
59
60
  const originalEnv = process.env.QUEUE_STRATEGY
60
61
  const getRedisUrlOrThrowMock = getRedisUrlOrThrow as jest.MockedFunction<typeof getRedisUrlOrThrow>
62
+ const parseRedisUrlMock = parseRedisUrl as jest.MockedFunction<typeof parseRedisUrl>
61
63
 
62
64
  beforeEach(() => {
63
65
  jest.clearAllMocks()
64
66
  getRedisUrlOrThrowMock.mockReturnValue('redis://localhost:6379')
67
+ parseRedisUrlMock.mockReturnValue({ host: 'localhost', port: 6379 })
65
68
  })
66
69
 
67
70
  afterEach(() => {
@@ -85,6 +88,7 @@ describe('createModuleQueue', () => {
85
88
  expect(queue.strategy).toBe('async')
86
89
  expect(queue.name).toBe('test-queue')
87
90
  expect(getRedisUrlOrThrowMock).toHaveBeenCalledWith('QUEUE')
91
+ expect(parseRedisUrlMock).toHaveBeenCalledWith('redis://localhost:6379')
88
92
  })
89
93
 
90
94
  it('passes concurrency to local strategy', () => {
@@ -2,7 +2,22 @@ import fs from 'node:fs'
2
2
  import os from 'node:os'
3
3
  import path from 'node:path'
4
4
  import { createQueue } from '../factory'
5
- import { getQueuePendingProbe } from '../pending-probe'
5
+ import { __resetPendingProbeBullMQCache, getQueuePendingProbe } from '../pending-probe'
6
+
7
+ const asyncQueueConstructor = jest.fn()
8
+ const asyncQueueClose = jest.fn(async () => {})
9
+ const asyncQueueGetJobCounts = jest.fn(async () => ({ waiting: 1, delayed: 0, active: 0 }))
10
+
11
+ jest.mock('bullmq', () => ({
12
+ Queue: class MockQueue {
13
+ constructor(name: string, options: unknown) {
14
+ asyncQueueConstructor(name, options)
15
+ }
16
+
17
+ getJobCounts = asyncQueueGetJobCounts
18
+ close = asyncQueueClose
19
+ },
20
+ }))
6
21
 
7
22
  describe('getQueuePendingProbe — local strategy', () => {
8
23
  const origCwd = process.cwd()
@@ -95,6 +110,11 @@ describe('getQueuePendingProbe — local strategy', () => {
95
110
  })
96
111
 
97
112
  describe('getQueuePendingProbe — async strategy', () => {
113
+ beforeEach(() => {
114
+ jest.clearAllMocks()
115
+ __resetPendingProbeBullMQCache()
116
+ })
117
+
98
118
  it('reports an error when QUEUE Redis URL is unset and no connection override is provided', async () => {
99
119
  const original = process.env.QUEUE_REDIS_URL
100
120
  const fallback = process.env.REDIS_URL
@@ -109,4 +129,23 @@ describe('getQueuePendingProbe — async strategy', () => {
109
129
  if (fallback !== undefined) process.env.REDIS_URL = fallback
110
130
  }
111
131
  })
132
+
133
+ it('converts a URL override to BullMQ connection fields', async () => {
134
+ const probe = await getQueuePendingProbe('async-probe', 'async', {
135
+ connection: { url: 'rediss://probe:secret@example.com:6380/3?family=6' },
136
+ })
137
+
138
+ expect(probe).toEqual(expect.objectContaining({ error: false, ready: 1 }))
139
+ expect(asyncQueueConstructor).toHaveBeenCalledWith('async-probe', {
140
+ connection: {
141
+ host: 'example.com',
142
+ port: 6380,
143
+ username: 'probe',
144
+ password: 'secret',
145
+ db: 3,
146
+ tls: {},
147
+ family: 6,
148
+ },
149
+ })
150
+ })
112
151
  })
@@ -0,0 +1,123 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { attachTraceMetadata, runJobInTrace } from '../tracing'
5
+ import { createLocalQueue } from '../strategies/local'
6
+ import { registerProvider, initTelemetry, shutdownTelemetry } from '@open-mercato/telemetry'
7
+ import type { LogRecord, MetricPoint, Span, SpanOptions, TelemetryProvider, TraceCarrier } from '@open-mercato/telemetry'
8
+
9
+ /**
10
+ * Verifies the enqueue → worker trace handoff: the active context is captured
11
+ * onto job metadata at enqueue, and the worker continues that trace at dispatch.
12
+ * Uses a recording provider under the explicitly enabled console seam rather
13
+ * than the real OTLP SDK.
14
+ */
15
+ const spanNames: string[] = []
16
+ const remoteCarriers: TraceCarrier[] = []
17
+
18
+ function noopSpan(): Span {
19
+ return { setAttribute() {}, setAttributes() {}, recordException() {}, setStatus() {}, end() {} }
20
+ }
21
+
22
+ const recordingProvider: TelemetryProvider = {
23
+ name: 'console',
24
+ supports: ['traces'],
25
+ async start() {},
26
+ async shutdown() {},
27
+ runInSpan<T>(name: string, _o: SpanOptions, fn: (s: Span) => T): T {
28
+ spanNames.push(name)
29
+ return fn(noopSpan())
30
+ },
31
+ activeSpan: () => undefined,
32
+ activeTraceContext: () => undefined,
33
+ inject: (carrier) => {
34
+ carrier.traceparent = 'test-traceparent'
35
+ },
36
+ runInRemoteSpan<T>(carrier: TraceCarrier, name: string, _o: SpanOptions, fn: (s: Span) => T): T {
37
+ remoteCarriers.push(carrier)
38
+ spanNames.push(name)
39
+ return fn(noopSpan())
40
+ },
41
+ emitLog: (_r: LogRecord) => {},
42
+ recordMetric: (_p: MetricPoint) => {},
43
+ }
44
+
45
+ beforeAll(async () => {
46
+ process.env.TELEMETRY_BACKEND = 'console'
47
+ registerProvider(recordingProvider)
48
+ await initTelemetry()
49
+ })
50
+
51
+ afterAll(async () => {
52
+ await shutdownTelemetry()
53
+ delete process.env.TELEMETRY_BACKEND
54
+ })
55
+
56
+ describe('queue trace propagation', () => {
57
+ it('attaches the active trace carrier to job metadata at enqueue', () => {
58
+ const metadata = attachTraceMetadata(undefined)
59
+ expect(metadata).toEqual({ _trace: { traceparent: 'test-traceparent' } })
60
+ })
61
+
62
+ it('preserves existing metadata while attaching the trace carrier', () => {
63
+ const metadata = attachTraceMetadata({ foo: 'bar' })
64
+ expect(metadata).toMatchObject({ foo: 'bar', _trace: { traceparent: 'test-traceparent' } })
65
+ })
66
+
67
+ it('continues the producer trace from job metadata at dispatch', async () => {
68
+ const result = await runJobInTrace('orders-process', { _trace: { traceparent: 'tp-123' } }, () =>
69
+ Promise.resolve('done'),
70
+ )
71
+ expect(result).toBe('done')
72
+ expect(remoteCarriers).toContainEqual({ traceparent: 'tp-123' })
73
+ expect(spanNames).toContain('queue.orders-process')
74
+ })
75
+
76
+ it('runs jobs without a carrier under a fresh span (no crash)', async () => {
77
+ const result = await runJobInTrace('orders-process', undefined, () => Promise.resolve(42))
78
+ expect(result).toBe(42)
79
+ })
80
+ })
81
+
82
+ /**
83
+ * End-to-end through the REAL local strategy (file write + read + dispatch),
84
+ * proving the headline acceptance criterion: a queued job continues the
85
+ * enqueuing request's trace. This exercises the actual enqueue/dispatch wiring,
86
+ * not just the helpers above.
87
+ */
88
+ describe('queue trace propagation (real local strategy)', () => {
89
+ let baseDir: string
90
+
91
+ beforeEach(() => {
92
+ baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'om-queue-trace-'))
93
+ })
94
+
95
+ afterEach(() => {
96
+ fs.rmSync(baseDir, { recursive: true, force: true })
97
+ })
98
+
99
+ it('persists the trace carrier on enqueue and continues it on dispatch', async () => {
100
+ const queue = createLocalQueue<{ orderId: string }>('orders-process', { baseDir })
101
+
102
+ await queue.enqueue({ orderId: 'o-1' })
103
+
104
+ // The carrier is written to the job's metadata — NOT the user payload.
105
+ const stored = JSON.parse(
106
+ fs.readFileSync(path.join(baseDir, 'orders-process', 'queue.json'), 'utf8'),
107
+ ) as Array<{ payload: unknown; metadata?: Record<string, unknown> }>
108
+ expect(stored[0].metadata).toEqual({ _trace: { traceparent: 'test-traceparent' } })
109
+ expect(stored[0].payload).toEqual({ orderId: 'o-1' })
110
+
111
+ let handlerRan = false
112
+ await queue.process((job) => {
113
+ handlerRan = true
114
+ // The handler still sees only its payload; the carrier is invisible to it.
115
+ expect(job.payload).toEqual({ orderId: 'o-1' })
116
+ })
117
+
118
+ expect(handlerRan).toBe(true)
119
+ // The worker continued the producer's trace under a `queue.<name>` span.
120
+ expect(remoteCarriers).toContainEqual({ traceparent: 'test-traceparent' })
121
+ expect(spanNames).toContain('queue.orders-process')
122
+ })
123
+ })
@@ -0,0 +1,80 @@
1
+ import path from 'node:path'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+
5
+ /**
6
+ * Regression: the worker's graceful shutdown used to close the queues
7
+ * and call process.exit() without flushing telemetry. A worker never returns
8
+ * from run(), so the CLI's post-run shutdownTelemetry() is unreachable — the
9
+ * BatchSpanProcessor's buffered tail (~5s of spans/logs) was dropped on every
10
+ * restart/redeploy. The shutdown handler must flush BEFORE exiting.
11
+ */
12
+
13
+ import {
14
+ registerTelemetryRuntime,
15
+ resetTelemetryRuntime,
16
+ type TelemetryRuntime,
17
+ } from '@open-mercato/shared/lib/telemetry/runtime'
18
+
19
+ import { runWorker } from '../worker/runner'
20
+
21
+ const mockCallOrder: string[] = []
22
+ const runtime: TelemetryRuntime = {
23
+ canUseGlobalTracePropagation: () => false,
24
+ captureTraceContext: () => ({}),
25
+ continueTrace: (_carrier, _name, fn) => fn(),
26
+ recordHttpDuration: () => {},
27
+ reportError: () => {},
28
+ shutdown: jest.fn(async () => {
29
+ mockCallOrder.push('flush')
30
+ }),
31
+ }
32
+
33
+ describe('worker shutdown flushes telemetry', () => {
34
+ let tmpDir: string
35
+ let cwd: string
36
+
37
+ beforeEach(() => {
38
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'worker-shutdown-'))
39
+ cwd = process.cwd()
40
+ process.chdir(tmpDir)
41
+ mockCallOrder.length = 0
42
+ delete process.env.TELEMETRY_BACKEND
43
+ registerTelemetryRuntime(runtime)
44
+ })
45
+
46
+ afterEach(() => {
47
+ process.chdir(cwd)
48
+ resetTelemetryRuntime()
49
+ fs.rmSync(tmpDir, { recursive: true, force: true })
50
+ })
51
+
52
+ it('SIGTERM flushes telemetry before process.exit', async () => {
53
+ const exitSpy = jest.spyOn(process, 'exit').mockImplementation(((code?: number) => {
54
+ mockCallOrder.push(`exit:${code ?? 0}`)
55
+ return undefined as never
56
+ }) as never)
57
+ const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {})
58
+
59
+ try {
60
+ await runWorker({
61
+ queueName: 'shutdown-flush-test',
62
+ handler: async () => {},
63
+ strategy: 'local',
64
+ background: true,
65
+ gracefulShutdown: true,
66
+ })
67
+
68
+ process.emit('SIGTERM')
69
+ // The shutdown handler is async (close → flush → exit); let it settle.
70
+ await new Promise((resolve) => setTimeout(resolve, 100))
71
+
72
+ expect(mockCallOrder).toContain('flush')
73
+ expect(mockCallOrder).toContain('exit:0')
74
+ expect(mockCallOrder.indexOf('flush')).toBeLessThan(mockCallOrder.indexOf('exit:0'))
75
+ } finally {
76
+ exitSpy.mockRestore()
77
+ logSpy.mockRestore()
78
+ }
79
+ })
80
+ })
package/src/factory.ts CHANGED
@@ -82,13 +82,16 @@ export function resolveQueueStrategy(): QueueStrategyType {
82
82
  */
83
83
  export function createModuleQueue<T = unknown>(
84
84
  name: string,
85
- options?: { concurrency?: number },
85
+ options?: Pick<AsyncQueueOptions, 'attempts' | 'concurrency' | 'lockDuration' | 'maxStalledCount'>,
86
86
  ): Queue<T> {
87
87
  const strategy = resolveQueueStrategy()
88
88
  if (strategy === 'async') {
89
89
  return createAsyncQueue<T>(name, {
90
90
  connection: { url: getRedisUrlOrThrow('QUEUE') },
91
91
  concurrency: options?.concurrency,
92
+ attempts: options?.attempts,
93
+ lockDuration: options?.lockDuration,
94
+ maxStalledCount: options?.maxStalledCount,
92
95
  })
93
96
  }
94
97
  return createLocalQueue<T>(name, { concurrency: options?.concurrency })