@open-mercato/queue 0.6.7-develop.6828.1.ab1620a63e → 0.6.7-develop.6842.1.8cd8a8883c
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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/pending-probe.js +4 -2
- package/dist/pending-probe.js.map +2 -2
- package/dist/strategies/async.js +38 -8
- package/dist/strategies/async.js.map +2 -2
- package/dist/strategies/local.js +14 -7
- package/dist/strategies/local.js.map +2 -2
- package/dist/tracing.js +27 -0
- package/dist/tracing.js.map +7 -0
- package/dist/worker/runner.js +13 -0
- package/dist/worker/runner.js.map +2 -2
- package/jest.config.cjs +1 -0
- package/package.json +8 -3
- package/src/__tests__/async.strategy.test.ts +30 -5
- package/src/__tests__/async.telemetry.test.ts +123 -0
- package/src/__tests__/factory.test.ts +5 -1
- package/src/__tests__/pending-probe.test.ts +40 -1
- package/src/__tests__/tracing.test.ts +123 -0
- package/src/__tests__/worker-shutdown-telemetry.test.ts +80 -0
- package/src/pending-probe.ts +4 -2
- package/src/strategies/async.ts +67 -12
- package/src/strategies/local.ts +11 -6
- package/src/tracing.ts +58 -0
- package/src/types.ts +2 -0
- package/src/worker/runner.ts +24 -0
package/src/strategies/local.ts
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path'
|
|
|
3
3
|
import crypto from 'node:crypto'
|
|
4
4
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
5
5
|
import type { Queue, QueuedJob, JobHandler, LocalQueueOptions, ProcessOptions, ProcessResult, EnqueueOptions, QueueJobScope } from '../types'
|
|
6
|
+
import { attachTraceMetadata, runJobInTrace } from '../tracing'
|
|
6
7
|
|
|
7
8
|
const packageLogger = createLogger('queue')
|
|
8
9
|
|
|
@@ -205,11 +206,13 @@ export function createLocalQueue<T = unknown>(
|
|
|
205
206
|
const availableAt = options?.delayMs && options.delayMs > 0
|
|
206
207
|
? new Date(Date.now() + options.delayMs).toISOString()
|
|
207
208
|
: undefined
|
|
209
|
+
const metadata = attachTraceMetadata(undefined)
|
|
208
210
|
const job: StoredJob<T> = {
|
|
209
211
|
id: generateId(),
|
|
210
212
|
payload: data,
|
|
211
213
|
createdAt: new Date().toISOString(),
|
|
212
214
|
...(availableAt ? { availableAt } : {}),
|
|
215
|
+
...(metadata ? { metadata } : {}),
|
|
213
216
|
}
|
|
214
217
|
await withFileLock(async () => {
|
|
215
218
|
const jobs = await readQueue()
|
|
@@ -255,12 +258,14 @@ export function createLocalQueue<T = unknown>(
|
|
|
255
258
|
for (const job of jobsToProcess) {
|
|
256
259
|
const attemptNumber = (job.attemptCount ?? 0) + 1
|
|
257
260
|
try {
|
|
258
|
-
await
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
261
|
+
await runJobInTrace(name, job.metadata, () =>
|
|
262
|
+
Promise.resolve(
|
|
263
|
+
handler(job, {
|
|
264
|
+
jobId: job.id,
|
|
265
|
+
attemptNumber,
|
|
266
|
+
queueName: name,
|
|
267
|
+
})
|
|
268
|
+
)
|
|
264
269
|
)
|
|
265
270
|
processed++
|
|
266
271
|
lastJobId = job.id
|
package/src/tracing.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
}
|
package/src/types.ts
CHANGED
package/src/worker/runner.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
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'
|
|
3
7
|
import type { Queue, JobHandler, AsyncQueueOptions, QueueStrategyType } from '../types'
|
|
4
8
|
|
|
5
9
|
const logger = createLogger('queue').child({ component: 'worker' })
|
|
@@ -71,6 +75,17 @@ function registerShutdownHandlers(): void {
|
|
|
71
75
|
unregisterShutdownHandlers(sigtermHandler, sigintHandler)
|
|
72
76
|
shutdownInProgress = false
|
|
73
77
|
|
|
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
|
+
|
|
74
89
|
if (!hasError) {
|
|
75
90
|
logger.info('Worker closed successfully')
|
|
76
91
|
}
|
|
@@ -141,6 +156,15 @@ export async function runWorker<T = unknown>(
|
|
|
141
156
|
strategy: strategyOption,
|
|
142
157
|
} = options
|
|
143
158
|
|
|
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
|
+
|
|
144
168
|
// Determine queue strategy from option, env var, or default to 'local'
|
|
145
169
|
const strategy: QueueStrategyType = strategyOption
|
|
146
170
|
?? (process.env.QUEUE_STRATEGY === 'async' ? 'async' : 'local')
|