Skip to content

Commit 4a4405f

Browse files
committed
3.2.0 - queue.process becomes child of queue.push span (producer-consumer link)
1 parent 233b991 commit 4a4405f

5 files changed

Lines changed: 125 additions & 7 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@prsm/queue",
3-
"version": "3.1.3",
3+
"version": "3.2.0",
44
"description": "Redis-backed distributed task queue with grouped concurrency, retries, and rate limiting",
55
"type": "module",
66
"exports": {

src/queue.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,13 +138,12 @@ export default class Queue extends EventEmitter {
138138
*/
139139
async push(payload, { group } = {}) {
140140
if (this._closed) throw new Error("Queue is closed")
141-
const traceparent = this._tracer ? this._tracer.toTraceparent() : null
142141
const task = group
143142
? { uuid: randomUUID(), payload, createdAt: Date.now(), group, attempts: 0 }
144143
: { uuid: randomUUID(), payload, createdAt: Date.now(), attempts: 0 }
145-
if (traceparent) task.traceparent = traceparent
146144
this._pushed++
147145
const span = this._tracer?.startSpan('queue.push', { 'queue.group': group ?? null, 'task.uuid': task.uuid }, { kind: 'producer' })
146+
if (span) task.traceparent = this._tracer.toTraceparent(span.context)
148147
try {
149148
await this._enqueue(task, group)
150149
} catch (err) {
@@ -165,11 +164,14 @@ export default class Queue extends EventEmitter {
165164
*/
166165
async pushAndWait(payload, { group, timeout = 0 } = {}) {
167166
if (this._closed) throw new Error("Queue is closed")
168-
const traceparent = this._tracer ? this._tracer.toTraceparent() : null
169167
const task = group
170168
? { uuid: randomUUID(), payload, createdAt: Date.now(), group, attempts: 0 }
171169
: { uuid: randomUUID(), payload, createdAt: Date.now(), attempts: 0 }
172-
if (traceparent) task.traceparent = traceparent
170+
const tpSpan = this._tracer?.startSpan('queue.pushAndWait', { 'queue.group': group ?? null, 'task.uuid': task.uuid }, { kind: 'producer' })
171+
if (tpSpan) {
172+
task.traceparent = this._tracer.toTraceparent(tpSpan.context)
173+
tpSpan.end()
174+
}
173175
this._pushed++
174176
const { promise, ready } = this._awaitTask(task.uuid, timeout)
175177
promise.catch(() => {})

tests/cross-instance-trace.test.js

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2+
import { createClient } from 'redis'
3+
import { createTracer } from '@prsm/trace'
4+
import Queue from '../src/index.js'
5+
6+
const REDIS = {}
7+
let admin
8+
9+
async function flush() {
10+
if (!admin) {
11+
admin = createClient()
12+
admin.on('error', () => {})
13+
await admin.connect()
14+
}
15+
await admin.flushAll()
16+
}
17+
18+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
19+
20+
const queues = []
21+
function make(opts) {
22+
const q = new Queue({ redisOptions: REDIS, ...opts })
23+
queues.push(q)
24+
return q
25+
}
26+
27+
beforeEach(async () => { await flush() })
28+
29+
afterEach(async () => {
30+
while (queues.length) await queues.pop().close().catch(() => {})
31+
})
32+
33+
describe('cross-instance trace propagation', () => {
34+
it('producer span on one instance, consumer span on another, share traceId', async () => {
35+
const tracer = createTracer({ service: 'svc' })
36+
const spans = []
37+
tracer.onSpan((s) => spans.push(s))
38+
39+
const producer = make({ tracer, concurrency: 0 })
40+
const consumer = make({ tracer, concurrency: 2 })
41+
42+
await producer.ready()
43+
await consumer.ready()
44+
45+
let processed = null
46+
consumer.process(async (payload) => { processed = payload; return { ok: true } })
47+
48+
let pushedTraceId = null
49+
await tracer.span('http.POST /order', async () => {
50+
pushedTraceId = tracer.current().traceId
51+
await producer.push({ orderId: 42 })
52+
})
53+
54+
await sleep(400)
55+
56+
expect(processed).toEqual({ orderId: 42 })
57+
58+
const pushSpan = spans.find((s) => s.name === 'queue.push')
59+
const processSpan = spans.find((s) => s.name === 'queue.process')
60+
expect(pushSpan).toBeTruthy()
61+
expect(processSpan).toBeTruthy()
62+
expect(pushSpan.traceId).toBe(pushedTraceId)
63+
expect(processSpan.traceId).toBe(pushedTraceId)
64+
expect(processSpan.parentSpanId).toBe(pushSpan.spanId)
65+
})
66+
67+
it('producer with no upstream context: push is root, process is its child', async () => {
68+
const tracer = createTracer({ service: 'svc' })
69+
const spans = []
70+
tracer.onSpan((s) => spans.push(s))
71+
72+
const producer = make({ tracer, concurrency: 0 })
73+
const consumer = make({ tracer, concurrency: 2 })
74+
await producer.ready()
75+
await consumer.ready()
76+
consumer.process(async () => 'done')
77+
78+
// push outside any tracer.span — push becomes the trace root
79+
await producer.push({ x: 1 })
80+
await sleep(300)
81+
82+
const pushSpan = spans.find((s) => s.name === 'queue.push')
83+
const processSpan = spans.find((s) => s.name === 'queue.process')
84+
expect(pushSpan).toBeTruthy()
85+
expect(processSpan).toBeTruthy()
86+
expect(pushSpan.parentSpanId).toBeNull()
87+
expect(processSpan.parentSpanId).toBe(pushSpan.spanId)
88+
expect(processSpan.traceId).toBe(pushSpan.traceId)
89+
})
90+
91+
it('grouped job carries trace context through the grouped queue path', async () => {
92+
const tracer = createTracer({ service: 'svc' })
93+
const spans = []
94+
tracer.onSpan((s) => spans.push(s))
95+
96+
const producer = make({ tracer, concurrency: 0 })
97+
const consumer = make({ tracer, groups: { concurrency: 1 } })
98+
await producer.ready()
99+
await consumer.ready()
100+
consumer.process(async () => 'done')
101+
102+
let pushedTraceId = null
103+
await tracer.span('grouped-entry', async () => {
104+
pushedTraceId = tracer.current().traceId
105+
await producer.push({ p: 1 }, { group: 'tenant-a' })
106+
})
107+
108+
await sleep(400)
109+
110+
const pushSpan = spans.find((s) => s.name === 'queue.push')
111+
const processSpan = spans.find((s) => s.name === 'queue.process')
112+
expect(pushSpan.traceId).toBe(pushedTraceId)
113+
expect(processSpan.traceId).toBe(pushedTraceId)
114+
})
115+
})

vitest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ export default {
22
test: {
33
reporters: ["verbose"],
44
testTimeout: 15000,
5+
fileParallelism: false,
56
},
67
}

0 commit comments

Comments
 (0)