Skip to content

Commit

Permalink
feat(utils): Add runBlocking that preserves Log4j's MDC context
Browse files Browse the repository at this point in the history
As `kotlinx.coroutines.runBlocking` creates a new coroutine context, it
does not preserve Log4j's MDC context [1] which is stored in a
thread-local `ThreadContext`. Add a helper function as replacement for
`runBlocking` which always adds a `CoroutineThreadContext` which is
initialized from the current `ThreadContext` to the newly created
coroutine context.

Signed-off-by: Martin Nonnenmacher <martin.nonnenmacher@bosch.com>
  • Loading branch information
mnonnenmacher authored and sschuberth committed Aug 20, 2024
1 parent f75bc26 commit d4d17d0
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 1 deletion.
2 changes: 1 addition & 1 deletion utils/ort/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ dependencies {
api(projects.utils.commonUtils)
api(projects.utils.spdxUtils)

api(libs.kotlinx.coroutines)
api(libs.okhttp)

implementation(libs.awsS3)
implementation(libs.commonsCompress)
implementation(libs.kotlinx.coroutines)

testImplementation(libs.mockk)
}
17 changes: 17 additions & 0 deletions utils/ort/src/main/kotlin/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ import java.net.Authenticator
import java.net.PasswordAuthentication
import java.net.URI

import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext

import kotlinx.coroutines.CoroutineScope

import org.apache.logging.log4j.kotlin.CoroutineThreadContext

import org.ossreviewtoolkit.utils.common.toSafeUri
import org.ossreviewtoolkit.utils.common.toUri
import org.ossreviewtoolkit.utils.common.withoutPrefix
Expand Down Expand Up @@ -211,3 +218,13 @@ fun normalizeVcsUrl(vcsUrl: String): String {

return url
}

/**
* A wrapper for [kotlinx.coroutines.runBlocking] which always adds a [CoroutineThreadContext] to the newly created
* coroutine context. This ensures that Log4j's MDC context is not lost when `runBlocking` is used.
*
* This function should be used instead of [kotlinx.coroutines.runBlocking] in all code that can be used as a library to
* preserve any MDC context set by a consumer.
*/
fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T =
kotlinx.coroutines.runBlocking(context + CoroutineThreadContext()) { block() }
20 changes: 20 additions & 0 deletions utils/ort/src/test/kotlin/UtilsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import io.kotest.inspectors.forAll
import io.kotest.matchers.collections.beEmpty
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.collections.shouldHaveSingleElement
import io.kotest.matchers.nulls.beNull
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
Expand All @@ -40,6 +41,11 @@ import java.net.PasswordAuthentication
import java.net.URI
import java.nio.file.Paths

import kotlin.coroutines.EmptyCoroutineContext

import org.apache.logging.log4j.kotlin.CoroutineThreadContext
import org.apache.logging.log4j.kotlin.withLoggingContext

class UtilsTest : WordSpec({
"filterVersionNames" should {
"return an empty list for a blank version" {
Expand Down Expand Up @@ -472,4 +478,18 @@ class UtilsTest : WordSpec({
}
}
}

"runBlocking" should {
"preserve Log4j's MDC context which kotlinx.coroutines.runBlocking does not" {
withLoggingContext(mapOf("key" to "value")) {
kotlinx.coroutines.runBlocking(EmptyCoroutineContext) {
coroutineContext[CoroutineThreadContext.Key]?.contextData?.map?.get("key") should beNull()
}

runBlocking(EmptyCoroutineContext) {
coroutineContext[CoroutineThreadContext.Key]?.contextData?.map?.get("key") shouldBe "value"
}
}
}
}
})

0 comments on commit d4d17d0

Please sign in to comment.