@@ -197,8 +197,91 @@ class KotlinLoggingAsyncMDCTest {
197197 jobB.join()
198198
199199 assertEquals(emptyMap(), requestAAfterBlock.await())
200- // THIS IS THE BUG: Request B sees "AAA " after its own block exits
200+ // THIS IS THE BUG: Request B sees "original " after its own block exits
201201 // It should be null (no context) but it's leaking Request A's value
202- assertEquals(mapOf (" foo" to " original" ), requestBAfterBlock.await()) // This will FAIL
202+ assertEquals(mapOf (" foo" to " original" ), requestBAfterBlock.await())
203+ }
204+
205+ @Test
206+ fun `withCoroutineLoggingContext does not context across coroutines` () = runTest {
207+ val dispatcher = Executors .newFixedThreadPool(1 ).asCoroutineDispatcher()
208+ // establish order of events with deferreds:
209+ // 1. request A will start, set its context, and then wait for request B (before exiting withLoggingContextAsync)
210+ // 2. request B will start, set its context, and then capture the MDC
211+ val requestAStarted = CompletableDeferred <Unit >()
212+ val requestBDone = CompletableDeferred <Unit >()
213+
214+ val requestAAfterBlock = CompletableDeferred <Map <String , String >? > ()
215+ val requestBAfterBlock = CompletableDeferred <Map <String , String >? > ()
216+
217+ val jobA = launch(dispatcher) {
218+ withCoroutineLoggingContext(" foo" to " original" ) {
219+ requestAStarted.complete(Unit )
220+ requestBDone.await() // Suspend while B runs
221+ }
222+ // will be empty
223+ requestAAfterBlock.complete(MDC .getCopyOfContextMap())
224+ }
225+
226+ val jobB = launch(dispatcher) {
227+ requestAStarted.await()
228+ withCoroutineLoggingContext(" foo" to " bar" ) { }
229+ // Capture MDC state immediately after Request B's block, will NOT be empty due to the bug
230+ requestBAfterBlock.complete(MDC .getCopyOfContextMap())
231+ requestBDone.complete(Unit )
232+ }
233+
234+ jobA.join()
235+ jobB.join()
236+
237+ assertEquals(emptyMap(), requestAAfterBlock.await())
238+ assertEquals(emptyMap(), requestBAfterBlock.await())
239+ }
240+
241+ @Test
242+ fun `map withCoroutineLoggingContext` () = runTest {
243+ assertNull(MDC .get(" a" ))
244+ assertNull(MDC .get(" c" ))
245+ assertNull(MDC .get(" e" ))
246+ assertNull(MDC .get(" f" ))
247+ assertNull(MDC .get(" k" ))
248+
249+ // original MDC outside of coroutine context
250+ MDC .put(" e" , " g" )
251+ MDC .put(" k" , " l" )
252+
253+ withCoroutineLoggingContext(mapOf (" a" to " b" , " c" to " d" , " e" to null , " f" to " h" )) {
254+ assertEquals(" b" , MDC .get(" a" ))
255+ assertEquals(" d" , MDC .get(" c" ))
256+ // does NOT inherit original MDC
257+ assertNull(MDC .get(" e" ))
258+ assertEquals(" h" , MDC .get(" f" ))
259+ // does NOT inherit original MDC
260+ assertNull(MDC .get(" k" ))
261+
262+ withCoroutineLoggingContext(mapOf (" a" to " b" , " e" to " i" , " f" to " j" )) {
263+ assertEquals(" b" , MDC .get(" a" ))
264+ // DOES inherit previous coroutine context
265+ assertEquals(" d" , MDC .get(" c" ))
266+ assertEquals(" i" , MDC .get(" e" ))
267+ assertEquals(" j" , MDC .get(" f" ))
268+ assertNull(MDC .get(" k" ))
269+ }
270+
271+ assertEquals(" b" , MDC .get(" a" ))
272+ assertEquals(" d" , MDC .get(" c" ))
273+ assertNull(MDC .get(" e" ))
274+ assertEquals(" h" , MDC .get(" f" ))
275+ assertNull(MDC .get(" k" ))
276+ }
277+
278+ assertNull(MDC .get(" a" ))
279+ assertNull(MDC .get(" c" ))
280+
281+ // original MDC is restored
282+ assertEquals(" g" , MDC .get(" e" ))
283+ assertNull(MDC .get(" f" ))
284+ // original MDC is restored
285+ assertEquals(" l" , MDC .get(" k" ))
203286 }
204287}
0 commit comments