@open-mercato/queue 0.6.7-develop.6862.1.c11a64ce0a → 0.6.7

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,9 +1,5 @@
1
1
  import { createQueue } from '../factory'
2
2
  import { createLogger } from '@open-mercato/shared/lib/logger'
3
- import {
4
- getTelemetryRuntime,
5
- isTelemetryBackendEnabled,
6
- } from '@open-mercato/shared/lib/telemetry/runtime'
7
3
  import type { Queue, JobHandler, AsyncQueueOptions, QueueStrategyType } from '../types'
8
4
 
9
5
  const logger = createLogger('queue').child({ component: 'worker' })
@@ -20,10 +16,6 @@ export type WorkerRunnerOptions<T = unknown> = {
20
16
  connection?: AsyncQueueOptions['connection']
21
17
  /** Number of concurrent jobs to process */
22
18
  concurrency?: number
23
- /** How long a job lock is held before the job counts as stalled, in ms. */
24
- lockDuration?: number
25
- /** Number of stalled-job recoveries BullMQ permits before failing a job. */
26
- maxStalledCount?: number
27
19
  /** Whether to set up graceful shutdown handlers */
28
20
  gracefulShutdown?: boolean
29
21
  /** If true, don't block - return immediately after starting processing (for multi-queue mode) */
@@ -75,17 +67,6 @@ function registerShutdownHandlers(): void {
75
67
  unregisterShutdownHandlers(sigtermHandler, sigintHandler)
76
68
  shutdownInProgress = false
77
69
 
78
- // Flush buffered spans/logs before the process dies. A worker never returns
79
- // from run(), so bin.ts's post-run shutdownTelemetry() is unreachable on this
80
- // path — without this, the BatchSpanProcessor's ~5s tail is dropped on every
81
- // restart/redeploy. Idempotent and a no-op when telemetry is off; a flush
82
- // failure must not turn a clean shutdown into a failed one.
83
- try {
84
- await getTelemetryRuntime()?.shutdown()
85
- } catch (error) {
86
- logger.error('Error flushing telemetry during shutdown', { err: error })
87
- }
88
-
89
70
  if (!hasError) {
90
71
  logger.info('Worker closed successfully')
91
72
  }
@@ -149,22 +130,11 @@ export async function runWorker<T = unknown>(
149
130
  handler,
150
131
  connection,
151
132
  concurrency = 1,
152
- lockDuration,
153
- maxStalledCount,
154
133
  gracefulShutdown = true,
155
134
  background = false,
156
135
  strategy: strategyOption,
157
136
  } = options
158
137
 
159
- // Worker processes don't run Next's instrumentation hook, so initialize
160
- // telemetry here — this is the single bootstrap every standalone worker passes
161
- // through. Import the telemetry package only for an explicit enabled backend;
162
- // with the default/unset backend the worker never evaluates the package.
163
- if (!getTelemetryRuntime() && isTelemetryBackendEnabled()) {
164
- const { initTelemetry } = await import('@open-mercato/telemetry')
165
- await initTelemetry()
166
- }
167
-
168
138
  // Determine queue strategy from option, env var, or default to 'local'
169
139
  const strategy: QueueStrategyType = strategyOption
170
140
  ?? (process.env.QUEUE_STRATEGY === 'async' ? 'async' : 'local')
@@ -174,8 +144,6 @@ export async function runWorker<T = unknown>(
174
144
  const queue = createQueue<T>(queueName, strategy, {
175
145
  connection,
176
146
  concurrency,
177
- lockDuration,
178
- maxStalledCount,
179
147
  })
180
148
 
181
149
  // Set up graceful shutdown
package/dist/tracing.js DELETED
@@ -1,27 +0,0 @@
1
- import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
2
- const TRACE_META_KEY = "_trace";
3
- function attachTraceMetadata(metadata) {
4
- const carrier = getTelemetryRuntime()?.captureTraceContext() ?? {};
5
- if (Object.keys(carrier).length === 0) return metadata;
6
- return { ...metadata ?? {}, [TRACE_META_KEY]: carrier };
7
- }
8
- function runJobInTrace(queueName, metadata, fn) {
9
- const runtime = getTelemetryRuntime();
10
- if (!runtime) return fn();
11
- return runtime.continueTrace(
12
- readTraceCarrier(metadata),
13
- `queue.${queueName}`,
14
- fn,
15
- { kind: "consumer" }
16
- );
17
- }
18
- function readTraceCarrier(metadata) {
19
- const raw = metadata?.[TRACE_META_KEY];
20
- if (!raw || typeof raw !== "object") return void 0;
21
- return raw;
22
- }
23
- export {
24
- attachTraceMetadata,
25
- runJobInTrace
26
- };
27
- //# sourceMappingURL=tracing.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/tracing.ts"],
4
- "sourcesContent": ["import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\n\n/**\n * Distributed-trace propagation across the enqueue \u2192 worker boundary.\n *\n * The W3C trace carrier rides on the job's `metadata._trace` (a first-class\n * metadata channel, not the user payload). Both halves are automatic \u2014 the\n * strategies call `attachTraceMetadata` at enqueue and `runJobInTrace` at\n * dispatch \u2014 so a worker joins the enqueuing request's trace with no per-worker\n * code. Everything here is a cheap no-op when telemetry is off.\n *\n * This also covers anything that rides the queue: persistent event subscribers\n * (the event bus enqueues) and outbound webhook delivery (queued) become part of\n * the originating request's trace for free.\n */\nconst TRACE_META_KEY = '_trace'\n\n/**\n * Attach the active trace context to a job's metadata. Returns `metadata`\n * unchanged when telemetry is off (no active span \u2192 empty carrier), so jobs stay\n * clean unless tracing is active.\n */\nexport function attachTraceMetadata(\n metadata: Record<string, unknown> | undefined,\n): Record<string, unknown> | undefined {\n const carrier = getTelemetryRuntime()?.captureTraceContext() ?? {}\n if (Object.keys(carrier).length === 0) return metadata\n return { ...(metadata ?? {}), [TRACE_META_KEY]: carrier }\n}\n\n/**\n * Run a job handler inside a span (`queue.<queueName>`) that continues the\n * producer's trace from the carrier on `metadata`. With no carrier (or telemetry\n * off) it runs `fn` under a fresh root span \u2014 and a no-op when off. The span\n * ends when `fn` settles (sync or async).\n */\nexport function runJobInTrace<T>(\n queueName: string,\n metadata: Record<string, unknown> | undefined,\n fn: () => T,\n): T {\n const runtime = getTelemetryRuntime()\n if (!runtime) return fn()\n return runtime.continueTrace(\n readTraceCarrier(metadata),\n `queue.${queueName}`,\n fn,\n { kind: 'consumer' },\n )\n}\n\nfunction readTraceCarrier(\n metadata: Record<string, unknown> | undefined,\n): Record<string, string> | undefined {\n const raw = metadata?.[TRACE_META_KEY]\n if (!raw || typeof raw !== 'object') return undefined\n return raw as Record<string, string>\n}\n"],
5
- "mappings": "AAAA,SAAS,2BAA2B;AAepC,MAAM,iBAAiB;AAOhB,SAAS,oBACd,UACqC;AACrC,QAAM,UAAU,oBAAoB,GAAG,oBAAoB,KAAK,CAAC;AACjE,MAAI,OAAO,KAAK,OAAO,EAAE,WAAW,EAAG,QAAO;AAC9C,SAAO,EAAE,GAAI,YAAY,CAAC,GAAI,CAAC,cAAc,GAAG,QAAQ;AAC1D;AAQO,SAAS,cACd,WACA,UACA,IACG;AACH,QAAM,UAAU,oBAAoB;AACpC,MAAI,CAAC,QAAS,QAAO,GAAG;AACxB,SAAO,QAAQ;AAAA,IACb,iBAAiB,QAAQ;AAAA,IACzB,SAAS,SAAS;AAAA,IAClB;AAAA,IACA,EAAE,MAAM,WAAW;AAAA,EACrB;AACF;AAEA,SAAS,iBACP,UACoC;AACpC,QAAM,MAAM,WAAW,cAAc;AACrC,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;AAC5C,SAAO;AACT;",
6
- "names": []
7
- }
@@ -1,123 +0,0 @@
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,123 +0,0 @@
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
- })
@@ -1,80 +0,0 @@
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/tracing.ts DELETED
@@ -1,58 +0,0 @@
1
- import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
2
-
3
- /**
4
- * Distributed-trace propagation across the enqueue → worker boundary.
5
- *
6
- * The W3C trace carrier rides on the job's `metadata._trace` (a first-class
7
- * metadata channel, not the user payload). Both halves are automatic — the
8
- * strategies call `attachTraceMetadata` at enqueue and `runJobInTrace` at
9
- * dispatch — so a worker joins the enqueuing request's trace with no per-worker
10
- * code. Everything here is a cheap no-op when telemetry is off.
11
- *
12
- * This also covers anything that rides the queue: persistent event subscribers
13
- * (the event bus enqueues) and outbound webhook delivery (queued) become part of
14
- * the originating request's trace for free.
15
- */
16
- const TRACE_META_KEY = '_trace'
17
-
18
- /**
19
- * Attach the active trace context to a job's metadata. Returns `metadata`
20
- * unchanged when telemetry is off (no active span → empty carrier), so jobs stay
21
- * clean unless tracing is active.
22
- */
23
- export function attachTraceMetadata(
24
- metadata: Record<string, unknown> | undefined,
25
- ): Record<string, unknown> | undefined {
26
- const carrier = getTelemetryRuntime()?.captureTraceContext() ?? {}
27
- if (Object.keys(carrier).length === 0) return metadata
28
- return { ...(metadata ?? {}), [TRACE_META_KEY]: carrier }
29
- }
30
-
31
- /**
32
- * Run a job handler inside a span (`queue.<queueName>`) that continues the
33
- * producer's trace from the carrier on `metadata`. With no carrier (or telemetry
34
- * off) it runs `fn` under a fresh root span — and a no-op when off. The span
35
- * ends when `fn` settles (sync or async).
36
- */
37
- export function runJobInTrace<T>(
38
- queueName: string,
39
- metadata: Record<string, unknown> | undefined,
40
- fn: () => T,
41
- ): T {
42
- const runtime = getTelemetryRuntime()
43
- if (!runtime) return fn()
44
- return runtime.continueTrace(
45
- readTraceCarrier(metadata),
46
- `queue.${queueName}`,
47
- fn,
48
- { kind: 'consumer' },
49
- )
50
- }
51
-
52
- function readTraceCarrier(
53
- metadata: Record<string, unknown> | undefined,
54
- ): Record<string, string> | undefined {
55
- const raw = metadata?.[TRACE_META_KEY]
56
- if (!raw || typeof raw !== 'object') return undefined
57
- return raw as Record<string, string>
58
- }