Skip to content

Commit ce11a98

Browse files
test(dsm): add regression test for DSM context leak in google-cloud-pubsub
Same root cause as the kafkajs test: ctx.currentStore is set by startSpan before decodeDataStreamsContext/setCheckpoint call enterWith, so the DSM context is never included in the bound store for async continuations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 57d5f96 commit ce11a98

1 file changed

Lines changed: 66 additions & 0 deletions

File tree

  • packages/datadog-plugin-google-cloud-pubsub/test

packages/datadog-plugin-google-cloud-pubsub/test/dsm.spec.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,72 @@ describe('Plugin', () => {
128128
})
129129
})
130130

131+
describe('concurrent context isolation', () => {
132+
it('Should maintain separate DSM context for interleaved consume-produce flows', async () => {
133+
const setCheckpointSpy = sinon.spy(DataStreamsProcessor.prototype, 'setCheckpoint')
134+
135+
try {
136+
const topicAIn = (await pubsub.createTopic(`dsm-iso-a-in-${id()}`))[0]
137+
const topicBIn = (await pubsub.createTopic(`dsm-iso-b-in-${id()}`))[0]
138+
const topicAOut = (await pubsub.createTopic(`dsm-iso-a-out-${id()}`))[0]
139+
const topicBOut = (await pubsub.createTopic(`dsm-iso-b-out-${id()}`))[0]
140+
141+
const subA = (await topicAIn.createSubscription(`sub-a-${id()}`))[0]
142+
const subB = (await topicBIn.createSubscription(`sub-b-${id()}`))[0]
143+
144+
const fullTopicAIn = topicAIn.metadata?.name || topicAIn.name
145+
const fullTopicBIn = topicBIn.metadata?.name || topicBIn.name
146+
const fullTopicAOut = topicAOut.metadata?.name || topicAOut.name
147+
const fullTopicBOut = topicBOut.metadata?.name || topicBOut.name
148+
149+
// Synchronization: both consumers must receive before either produces
150+
let resolveAEntered, resolveBEntered
151+
const aEntered = new Promise(resolve => { resolveAEntered = resolve })
152+
const bEntered = new Promise(resolve => { resolveBEntered = resolve })
153+
let doneCount = 0
154+
const allDone = new Promise(resolve => {
155+
const check = () => { if (++doneCount === 2) resolve() }
156+
subA.on('message', async (msg) => {
157+
msg.ack()
158+
resolveAEntered()
159+
await bEntered
160+
await publish(topicAOut, { data: Buffer.from('from-a') })
161+
check()
162+
})
163+
subB.on('message', async (msg) => {
164+
msg.ack()
165+
resolveBEntered()
166+
await aEntered
167+
await publish(topicBOut, { data: Buffer.from('from-b') })
168+
check()
169+
})
170+
})
171+
172+
await publish(topicAIn, { data: Buffer.from('msg-a') })
173+
await publish(topicBIn, { data: Buffer.from('msg-b') })
174+
175+
await allDone
176+
177+
const calls = setCheckpointSpy.getCalls()
178+
const checkpoint = (dir, topic) => calls.find(c =>
179+
c.args[0].includes(`direction:${dir}`) && c.args[0].includes(`topic:${topic}`)
180+
)
181+
182+
const consumeA = checkpoint('in', fullTopicAIn)
183+
const consumeB = checkpoint('in', fullTopicBIn)
184+
const produceA = checkpoint('out', fullTopicAOut)
185+
const produceB = checkpoint('out', fullTopicBOut)
186+
187+
assert.ok(produceA?.args[2], 'Process A produce should have a parent DSM context')
188+
assert.ok(produceB?.args[2], 'Process B produce should have a parent DSM context')
189+
assert.deepStrictEqual(produceA.args[2].hash, consumeA.returnValue.hash)
190+
assert.deepStrictEqual(produceB.args[2].hash, consumeB.returnValue.hash)
191+
} finally {
192+
setCheckpointSpy.restore()
193+
}
194+
})
195+
})
196+
131197
describe('it should set a message payload size', () => {
132198
let recordCheckpointSpy
133199

0 commit comments

Comments
 (0)