Skip to content

Commit be64f7a

Browse files
maniax89Andrew Turgeon
authored andcommitted
test(jvm): demonstrate withLoggingContextAsync bug
1 parent b1eb9fc commit be64f7a

1 file changed

Lines changed: 43 additions & 0 deletions

File tree

src/jvmTest/kotlin/io/github/oshai/kotlinlogging/coroutines/KotlinLoggingAsyncMDCTest.kt

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
package io.github.oshai.kotlinlogging.coroutines
22

3+
import kotlinx.coroutines.CompletableDeferred
34
import kotlin.test.*
45
import kotlinx.coroutines.ExperimentalCoroutinesApi
6+
import kotlinx.coroutines.asCoroutineDispatcher
7+
import kotlinx.coroutines.launch
8+
import kotlinx.coroutines.runBlocking
59
import kotlinx.coroutines.test.runTest
610
import org.apache.logging.log4j.*
711
import org.apache.logging.log4j.core.config.*
812
import org.junit.jupiter.api.BeforeEach
913
import org.slf4j.*
14+
import java.util.concurrent.Executors
1015

1116
@ExperimentalCoroutinesApi
1217
class KotlinLoggingAsyncMDCTest {
@@ -158,4 +163,42 @@ class KotlinLoggingAsyncMDCTest {
158163
assertNull(MDC.get("f"))
159164
assertEquals("l", MDC.get("k"))
160165
}
166+
167+
@Test
168+
fun `withLoggingContextAsync leaks context across coroutines`() = runTest {
169+
val dispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
170+
// establish order of events with deferreds:
171+
// 1. request A will start, set its context, and then wait for request B (before exiting 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 requestAAfterBlock = CompletableDeferred<Map<String, String>?>()
177+
val requestBAfterBlock = CompletableDeferred<Map<String, String>?>()
178+
179+
val jobA = launch(dispatcher) {
180+
withLoggingContextAsync("foo" to "original") {
181+
requestAStarted.complete(Unit)
182+
requestBDone.await() // Suspend while B runs
183+
}
184+
// will be empty
185+
requestAAfterBlock.complete(MDC.getCopyOfContextMap())
186+
}
187+
188+
val jobB = launch(dispatcher) {
189+
requestAStarted.await()
190+
withLoggingContextAsync("foo" to "bar") { }
191+
// Capture MDC state immediately after Request B's block, will NOT be empty due to the bug
192+
requestBAfterBlock.complete(MDC.getCopyOfContextMap())
193+
requestBDone.complete(Unit)
194+
}
195+
196+
jobA.join()
197+
jobB.join()
198+
199+
assertEquals(emptyMap(), requestAAfterBlock.await())
200+
// THIS IS THE BUG: Request B sees "AAA" after its own block exits
201+
// 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
203+
}
161204
}

0 commit comments

Comments
 (0)