@@ -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