@@ -163,98 +163,103 @@ class KotlinLoggingAsyncMDCTest {
163163 assertEquals(" l" , MDC .get(" k" ))
164164 }
165165
166+
166167 @Test
167- fun `withLoggingContextAsync leaks context across coroutines` () = runTest {
168+ fun `withLoggingContextAsync leaks context across coroutines per GH issue 75 ` () = runTest {
168169 val dispatcher = Executors .newFixedThreadPool(1 ).asCoroutineDispatcher()
169- // establish order of events with deferreds:
170- // 1. request A will start, set its context, and then wait for request B (before exiting
171- // withLoggingContextAsync)
172- // 2. request B will start, set its context, and then capture the MDC
173- val requestAStarted = CompletableDeferred <Unit >()
174- val requestBDone = CompletableDeferred <Unit >()
175-
176- val requestADuringBlock = CompletableDeferred <Map <String , String >? > ()
177- val requestAAfterBlock = CompletableDeferred <Map <String , String >? > ()
178- val requestBDuringBlock = CompletableDeferred <Map <String , String >? > ()
179- val requestBAfterBlock = CompletableDeferred <Map <String , String >? > ()
180-
181- val jobA =
182- launch(dispatcher) {
183- withLoggingContextAsync(" foo" to " original" ) {
184- requestAStarted.complete(Unit )
185- requestADuringBlock.complete(MDC .getCopyOfContextMap())
186- requestBDone.await() // Suspend while B runs
170+ dispatcher.use {
171+ // establish order of events with deferreds:
172+ // 1. request A will start, set its context, and then wait for request B (before exiting
173+ // withLoggingContextAsync)
174+ // 2. request B will start, set its context, and then capture the MDC
175+ val requestAStarted = CompletableDeferred <Unit >()
176+ val requestBDone = CompletableDeferred <Unit >()
177+
178+ val requestADuringBlock = CompletableDeferred <Map <String , String >>()
179+ val requestAAfterBlock = CompletableDeferred <Map <String , String >>()
180+ val requestBDuringBlock = CompletableDeferred <Map <String , String >>()
181+ val requestBAfterBlock = CompletableDeferred <Map <String , String >>()
182+
183+ val jobA =
184+ launch(dispatcher) {
185+ withLoggingContextAsync(" foo" to " original" ) {
186+ requestAStarted.complete(Unit )
187+ requestADuringBlock.complete(MDC .getCopyOfContextMap() ? : emptyMap())
188+ requestBDone.await() // Suspend while B runs
189+ }
190+ // will be empty
191+ requestAAfterBlock.complete(MDC .getCopyOfContextMap() ? : emptyMap())
187192 }
188- // will be empty
189- requestAAfterBlock.complete(MDC .getCopyOfContextMap())
190- }
191193
192- val jobB =
193- launch(dispatcher) {
194- requestAStarted.await()
195- withLoggingContextAsync(" foo" to " bar" ) {
196- requestBDuringBlock.complete(MDC .getCopyOfContextMap())
194+ val jobB =
195+ launch(dispatcher) {
196+ requestAStarted.await()
197+ withLoggingContextAsync(" foo" to " bar" ) {
198+ requestBDuringBlock.complete(MDC .getCopyOfContextMap() ? : emptyMap())
199+ }
200+ // Capture MDC state immediately after Request B's block, will NOT be empty due to the bug
201+ requestBAfterBlock.complete(MDC .getCopyOfContextMap() ? : emptyMap())
202+ requestBDone.complete(Unit )
197203 }
198- // Capture MDC state immediately after Request B's block, will NOT be empty due to the bug
199- requestBAfterBlock.complete(MDC .getCopyOfContextMap())
200- requestBDone.complete(Unit )
201- }
202204
203- jobA.join()
204- jobB.join()
205+ jobA.join()
206+ jobB.join()
205207
206- assertEquals(mapOf (" foo" to " original" ), requestADuringBlock.await())
207- assertEquals(emptyMap(), requestAAfterBlock.await())
208- assertEquals(mapOf (" foo" to " bar" ), requestBDuringBlock.await())
209- // THIS IS THE BUG: Request B sees "original" after its own block exits
210- // It should be null (no context) but it's leaking Request A's value
211- assertEquals(mapOf (" foo" to " original" ), requestBAfterBlock.await())
208+ assertEquals(mapOf (" foo" to " original" ), requestADuringBlock.await())
209+ assertEquals(emptyMap(), requestAAfterBlock.await())
210+ assertEquals(mapOf (" foo" to " bar" ), requestBDuringBlock.await())
211+ // THIS IS THE BUG: Request B sees "original" after its own block exits
212+ // It should be null (no context) but it's leaking Request A's value
213+ assertEquals(mapOf (" foo" to " original" ), requestBAfterBlock.await())
214+ }
212215 }
213216
214217 @Test
215- fun `withCoroutineLoggingContext does not leak context across coroutines ` () = runTest {
218+ fun `withCoroutineLoggingContext binds logging context to current coroutine contextMap ` () = runTest {
216219 val dispatcher = Executors .newFixedThreadPool(1 ).asCoroutineDispatcher()
217- // establish order of events with deferreds:
218- // 1. request A will start, set its context, and then wait for request B (before exiting
219- // withLoggingContextAsync)
220- // 2. request B will start, set its context, and then capture the MDC
221- val requestAStarted = CompletableDeferred <Unit >()
222- val requestBDone = CompletableDeferred <Unit >()
223-
224- val requestADuringBlock = CompletableDeferred <Map <String , String >? > ()
225- val requestAAfterBlock = CompletableDeferred <Map <String , String >? > ()
226- val requestBDuringBlock = CompletableDeferred <Map <String , String >? > ()
227- val requestBAfterBlock = CompletableDeferred <Map <String , String >? > ()
228-
229- val jobA =
230- launch(dispatcher) {
231- withCoroutineLoggingContext(" foo" to " original" ) {
232- requestAStarted.complete(Unit )
233- requestADuringBlock.complete(MDC .getCopyOfContextMap())
234- requestBDone.await() // Suspend while B runs
220+ dispatcher.use {
221+ // establish order of events with deferreds:
222+ // 1. request A will start, set its context, and then wait for request B (before exiting
223+ // withLoggingContextAsync)
224+ // 2. request B will start, set its context, and then capture the MDC
225+ val requestAStarted = CompletableDeferred <Unit >()
226+ val requestBDone = CompletableDeferred <Unit >()
227+
228+ val requestADuringBlock = CompletableDeferred <Map <String , String >>()
229+ val requestAAfterBlock = CompletableDeferred <Map <String , String >>()
230+ val requestBDuringBlock = CompletableDeferred <Map <String , String >>()
231+ val requestBAfterBlock = CompletableDeferred <Map <String , String >>()
232+
233+ val jobA =
234+ launch(dispatcher) {
235+ withCoroutineLoggingContext(" foo" to " original" ) {
236+ requestAStarted.complete(Unit )
237+ requestADuringBlock.complete(MDC .getCopyOfContextMap() ? : emptyMap())
238+ requestBDone.await() // Suspend while B runs
239+ }
240+ // will be empty
241+ requestAAfterBlock.complete(MDC .getCopyOfContextMap() ? : emptyMap())
235242 }
236- // will be empty
237- requestAAfterBlock.complete(MDC .getCopyOfContextMap())
238- }
239243
240- val jobB =
241- launch(dispatcher) {
242- requestAStarted.await()
243- withCoroutineLoggingContext(" foo" to " bar" ) {
244- requestBDuringBlock.complete(MDC .getCopyOfContextMap())
244+ val jobB =
245+ launch(dispatcher) {
246+ requestAStarted.await()
247+ withCoroutineLoggingContext(" foo" to " bar" ) {
248+ requestBDuringBlock.complete(MDC .getCopyOfContextMap() ? : emptyMap())
249+ }
250+ // will also be empty
251+ requestBAfterBlock.complete(MDC .getCopyOfContextMap() ? : emptyMap())
252+ requestBDone.complete(Unit )
245253 }
246- // will also be empty
247- requestBAfterBlock.complete(MDC .getCopyOfContextMap())
248- requestBDone.complete(Unit )
249- }
250254
251- jobA.join()
252- jobB.join()
255+ jobA.join()
256+ jobB.join()
253257
254- assertEquals(mapOf (" foo" to " original" ), requestADuringBlock.await())
255- assertEquals(emptyMap(), requestAAfterBlock.await())
256- assertEquals(mapOf (" foo" to " bar" ), requestBDuringBlock.await())
257- assertEquals(emptyMap(), requestBAfterBlock.await())
258+ assertEquals(mapOf (" foo" to " original" ), requestADuringBlock.await())
259+ assertEquals(emptyMap(), requestAAfterBlock.await())
260+ assertEquals(mapOf (" foo" to " bar" ), requestBDuringBlock.await())
261+ assertEquals(emptyMap(), requestBAfterBlock.await())
262+ }
258263 }
259264
260265 @Test
0 commit comments