Skip to content

Commit 2faed3c

Browse files
committed
Refine PropagationContextElement
This commit apply several refinements to PropagationContextElement: - Capture the ThreadLocal when instantiating the PropagationContextElement in order to support dispatchers switching threads - Remove the constructor parameter which is not idiomatic and breaks the support when switching threads, and use instead the updateThreadContext(context: CoroutineContext) parameter - Make the kotlinx-coroutines-reactor dependency optional - Make the properties private The Javadoc and tests are also updated to use the `Dispatchers.IO + PropagationContextElement()` pattern performed outside of the suspending lambda, which is the typical use case. Closes gh-35469
1 parent 20e1149 commit 2faed3c

File tree

4 files changed

+81
-76
lines changed

4 files changed

+81
-76
lines changed

framework-docs/modules/ROOT/pages/languages/kotlin/coroutines.adoc

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -257,29 +257,20 @@ For Kotlin `Flow`, a `Flow<T>.transactional` extension is provided.
257257
Spring applications are xref:integration/observability.adoc[instrumented with Micrometer for Observability support].
258258
For tracing support, the current observation is propagated through a `ThreadLocal` for blocking code,
259259
or the Reactor `Context` for reactive pipelines. But the current observation also needs to be made available
260-
in the execution context of a suspended function. Without that, the current "traceId" will not be automatically prepended
261-
to logged statements from coroutines.
260+
in the execution context of a suspended function. Without that, the current "traceId" will not be automatically
261+
prepended to logged statements from coroutines.
262262

263-
The `org.springframework.core.PropagationContextElement` operator generally ensures that the
263+
The {spring-framework-api-kdoc}/spring-core/org.springframework.core/-propagation-context-element/index.html[`PropagationContextElement`] operator generally ensures that the
264264
{micrometer-context-propagation-docs}/[Micrometer Context Propagation library] works with Kotlin Coroutines.
265265

266-
The `PropagationContextElement` requires the following dependencies:
266+
It requires the `io.micrometer:context-propagation` dependency and optionally the
267+
`org.jetbrains.kotlinx:kotlinx-coroutines-reactor` one.
267268

268-
`build.gradle.kts`
269-
[source,kotlin,indent=0]
270-
----
271-
dependencies {
272-
implementation("io.micrometer:context-propagation:${contextPropagationVersion}")
273-
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}")
274-
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:${coroutinesVersion}")
275-
}
276-
----
277-
278-
Applications can then use the `PropagationContextElement` operator to connect the `currentCoroutineContext()`
269+
Applications can then use the `PropagationContextElement` to augment the `CoroutineContext`
279270
with the context propagation mechanism:
280271

281272
include-code::./ContextPropagationSample[tag=context,indent=0]
282273

283-
Here, assuming that Micrometer Tracing is configured, the resulting logging statement
284-
will show the current "traceId" and unlock better observability for your application.
274+
Here, assuming that Micrometer Tracing is configured, the resulting logging statement will show the current "traceId"
275+
and unlock better observability for your application.
285276

framework-docs/src/main/kotlin/org/springframework/docs/languages/kotlin/coroutines/propagation/ContextPropagationSample.kt

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616

1717
package org.springframework.docs.languages.kotlin.coroutines.propagation
1818

19-
import kotlinx.coroutines.currentCoroutineContext
20-
import kotlinx.coroutines.withContext
19+
import kotlinx.coroutines.Dispatchers
20+
import kotlinx.coroutines.delay
21+
import kotlinx.coroutines.runBlocking
2122
import org.apache.commons.logging.Log
2223
import org.apache.commons.logging.LogFactory
2324
import org.springframework.core.PropagationContextElement
@@ -31,10 +32,15 @@ class ContextPropagationSample {
3132
}
3233

3334
// tag::context[]
35+
fun main() {
36+
runBlocking(Dispatchers.IO + PropagationContextElement()) {
37+
suspendingFunction()
38+
}
39+
}
40+
3441
suspend fun suspendingFunction() {
35-
return withContext(PropagationContextElement(currentCoroutineContext())) {
36-
logger.info("Suspending function with traceId")
37-
}
42+
delay(1)
43+
logger.info("Suspending function with traceId")
3844
}
3945
// end::context[]
4046
}

spring-core/src/main/kotlin/org/springframework/core/PropagationContextElement.kt

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,55 +21,78 @@ import io.micrometer.context.ContextSnapshot
2121
import io.micrometer.context.ContextSnapshotFactory
2222
import kotlinx.coroutines.ThreadContextElement
2323
import kotlinx.coroutines.reactor.ReactorContext
24+
import org.springframework.util.ClassUtils
2425
import reactor.util.context.ContextView
2526
import kotlin.coroutines.AbstractCoroutineContextElement
2627
import kotlin.coroutines.CoroutineContext
2728

2829

2930
/**
30-
* [ThreadContextElement] that restores `ThreadLocals` from the Reactor [ContextSnapshot]
31-
* every time the coroutine with this element in the context is resumed on a thread.
31+
* [ThreadContextElement] that ensures that contexts registered with the
32+
* Micrometer Context Propagation library are captured and restored when
33+
* a coroutine is resumed on a thread. This is typically being used for
34+
* Micrometer Tracing support in Kotlin suspended functions.
3235
*
33-
* This effectively ensures that Kotlin Coroutines, Reactor and Micrometer Context Propagation
34-
* work together in an application, typically for observability purposes.
36+
* It requires the `io.micrometer:context-propagation` library. If the
37+
* `org.jetbrains.kotlinx:kotlinx-coroutines-reactor` dependency is also
38+
* on the classpath, this element also supports Reactor `Context`.
3539
*
36-
* Applications need to have both `"io.micrometer:context-propagation"` and
37-
* `"org.jetbrains.kotlinx:kotlinx-coroutines-reactor"` on the classpath to use this context element.
40+
* `PropagationContextElement` can be used like this:
3841
*
39-
* The `PropagationContextElement` can be used like this:
40-
*
4142
* ```kotlin
42-
* suspend fun suspendable() {
43-
* withContext(PropagationContextElement(coroutineContext)) {
44-
* logger.info("Log statement with traceId")
45-
* }
43+
* fun main() {
44+
* runBlocking(Dispatchers.IO + PropagationContextElement()) {
45+
* suspendingFunction()
46+
* }
4647
* }
48+
*
49+
* suspend fun suspendingFunction() {
50+
* delay(1)
51+
* logger.info("Log statement with traceId")
52+
* }
4753
* ```
4854
*
4955
* @author Brian Clozel
56+
* @author Sebastien Deleuze
5057
* @since 7.0
5158
*/
52-
class PropagationContextElement(private val context: CoroutineContext) : ThreadContextElement<ContextSnapshot.Scope>,
59+
class PropagationContextElement : ThreadContextElement<ContextSnapshot.Scope>,
5360
AbstractCoroutineContextElement(Key) {
5461

55-
companion object Key : CoroutineContext.Key<PropagationContextElement>
62+
companion object Key : CoroutineContext.Key<PropagationContextElement> {
5663

57-
val contextSnapshot: ContextSnapshot
58-
get() {
59-
val contextView: ContextView? = context[ReactorContext]?.context
60-
val contextSnapshotFactory =
61-
ContextSnapshotFactory.builder().contextRegistry(ContextRegistry.getInstance()).build()
62-
if (contextView != null) {
63-
return contextSnapshotFactory.captureFrom(contextView)
64-
}
65-
return contextSnapshotFactory.captureAll()
66-
}
64+
private val contextSnapshotFactory =
65+
ContextSnapshotFactory.builder().contextRegistry(ContextRegistry.getInstance()).build()
66+
67+
private val coroutinesReactorPresent =
68+
ClassUtils.isPresent("kotlinx.coroutines.reactor.ReactorContext",
69+
PropagationContextElement::class.java.classLoader);
70+
}
71+
72+
// Context captured from the the ThreadLocal where the PropagationContextElement is instantiated
73+
private val threadLocalContextSnapshot: ContextSnapshot = contextSnapshotFactory.captureAll()
6774

6875
override fun restoreThreadContext(context: CoroutineContext, oldState: ContextSnapshot.Scope) {
6976
oldState.close()
7077
}
7178

7279
override fun updateThreadContext(context: CoroutineContext): ContextSnapshot.Scope {
80+
val contextSnapshot = if (coroutinesReactorPresent) {
81+
ReactorDelegate().captureFrom(context) ?: threadLocalContextSnapshot
82+
} else {
83+
threadLocalContextSnapshot
84+
}
7385
return contextSnapshot.setThreadLocals()
7486
}
75-
}
87+
88+
private class ReactorDelegate {
89+
90+
fun captureFrom(context: CoroutineContext): ContextSnapshot? {
91+
val contextView: ContextView? = context[ReactorContext]?.context
92+
if (contextView != null) {
93+
return contextSnapshotFactory.captureFrom(contextView)
94+
}
95+
return null;
96+
}
97+
}
98+
}

spring-core/src/test/kotlin/org/springframework/core/PropagationContextElementTests.kt

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -19,51 +19,33 @@ package org.springframework.core
1919
import io.micrometer.observation.Observation
2020
import io.micrometer.observation.tck.TestObservationRegistry
2121
import kotlinx.coroutines.Dispatchers
22-
import kotlinx.coroutines.currentCoroutineContext
22+
import kotlinx.coroutines.delay
2323
import kotlinx.coroutines.runBlocking
24-
import kotlinx.coroutines.withContext
2524
import org.assertj.core.api.Assertions
2625
import org.assertj.core.api.Assertions.assertThat
27-
import org.junit.jupiter.api.AfterAll
28-
import org.junit.jupiter.api.BeforeAll
2926
import org.junit.jupiter.api.Test
3027
import org.reactivestreams.Publisher
3128
import reactor.core.publisher.Hooks
3229
import reactor.core.publisher.Mono
33-
import reactor.core.scheduler.Schedulers
3430
import kotlin.coroutines.Continuation
3531

3632

3733
/**
3834
* Kotlin tests for [PropagationContextElement].
3935
*
4036
* @author Brian Clozel
37+
* @author Sebastien Deleuze
4138
*/
4239
class PropagationContextElementTests {
4340

4441
private val observationRegistry = TestObservationRegistry.create()
4542

46-
companion object {
47-
48-
@BeforeAll
49-
@JvmStatic
50-
fun init() {
51-
Hooks.enableAutomaticContextPropagation()
52-
}
53-
54-
@AfterAll
55-
@JvmStatic
56-
fun cleanup() {
57-
Hooks.disableAutomaticContextPropagation()
58-
}
59-
60-
}
61-
6243
@Test
6344
fun restoresFromThreadLocal() {
6445
val observation = Observation.createNotStarted("coroutine", observationRegistry)
6546
observation.observe {
66-
val result = runBlocking(Dispatchers.Unconfined) {
47+
val coroutineContext = Dispatchers.IO + PropagationContextElement()
48+
val result = runBlocking(coroutineContext) {
6749
suspendingFunction("test")
6850
}
6951
Assertions.assertThat(result).isEqualTo("coroutine")
@@ -74,20 +56,23 @@ class PropagationContextElementTests {
7456
@Suppress("UNCHECKED_CAST")
7557
fun restoresFromReactorContext() {
7658
val method = PropagationContextElementTests::class.java.getDeclaredMethod("suspendingFunction", String::class.java, Continuation::class.java)
77-
val publisher = CoroutinesUtils.invokeSuspendingFunction(method, this, "test", null) as Publisher<String>
59+
val coroutineContext = Dispatchers.IO + PropagationContextElement()
60+
val publisher = CoroutinesUtils.invokeSuspendingFunction(coroutineContext, method, this, "test", null) as Publisher<String>
7861
val observation = Observation.createNotStarted("coroutine", observationRegistry)
62+
Hooks.enableAutomaticContextPropagation()
7963
observation.observe {
80-
val result = Mono.from<String>(publisher).publishOn(Schedulers.boundedElastic()).block()
64+
val mono = Mono.from<String>(publisher)
65+
val result = mono.block()
8166
assertThat(result).isEqualTo("coroutine")
8267
}
68+
Hooks.disableAutomaticContextPropagation()
8369
}
8470

8571
suspend fun suspendingFunction(value: String): String? {
86-
return withContext(PropagationContextElement(currentCoroutineContext())) {
87-
val currentObservation = observationRegistry.currentObservation
88-
assertThat(currentObservation).isNotNull
89-
currentObservation?.context?.name
90-
}
72+
delay(1)
73+
val currentObservation = observationRegistry.currentObservation
74+
assertThat(currentObservation).isNotNull
75+
return currentObservation?.context?.name
9176
}
9277

9378
}

0 commit comments

Comments
 (0)