Skip to content

Commit e986e7d

Browse files
committed
Add Kotlin Coroutines CDI Context Propagation
1 parent 945c730 commit e986e7d

File tree

7 files changed

+854
-0
lines changed

7 files changed

+854
-0
lines changed

bom/application/pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,16 @@
613613
<artifactId>quarkus-arc</artifactId>
614614
<version>${project.version}</version>
615615
</dependency>
616+
<dependency>
617+
<groupId>io.quarkus</groupId>
618+
<artifactId>quarkus-arc-kotlin</artifactId>
619+
<version>${project.version}</version>
620+
</dependency>
621+
<dependency>
622+
<groupId>io.quarkus</groupId>
623+
<artifactId>quarkus-arc-tests</artifactId>
624+
<version>${project.version}</version>
625+
</dependency>
616626
<dependency>
617627
<groupId>io.quarkus</groupId>
618628
<artifactId>quarkus-arc-dev</artifactId>

extensions/arc/kotlin/pom.xml

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<parent>
7+
<groupId>io.quarkus</groupId>
8+
<artifactId>quarkus-arc-parent</artifactId>
9+
<version>999-SNAPSHOT</version>
10+
</parent>
11+
12+
<artifactId>quarkus-arc-kotlin</artifactId>
13+
<name>Quarkus - Arc - Kotlin</name>
14+
15+
<dependencies>
16+
<dependency>
17+
<groupId>io.quarkus.arc</groupId>
18+
<artifactId>arc</artifactId>
19+
</dependency>
20+
21+
<dependency>
22+
<groupId>org.jetbrains.kotlin</groupId>
23+
<artifactId>kotlin-stdlib-jdk8</artifactId>
24+
<optional>true</optional>
25+
</dependency>
26+
<dependency>
27+
<groupId>org.jetbrains.kotlinx</groupId>
28+
<artifactId>kotlinx-coroutines-core</artifactId>
29+
</dependency>
30+
<dependency>
31+
<groupId>org.jetbrains.kotlinx</groupId>
32+
<artifactId>kotlinx-coroutines-core-jvm</artifactId>
33+
</dependency>
34+
<dependency>
35+
<groupId>org.jetbrains.kotlinx</groupId>
36+
<artifactId>kotlinx-coroutines-jdk8</artifactId>
37+
</dependency>
38+
39+
40+
</dependencies>
41+
42+
<build>
43+
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
44+
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
45+
<plugins>
46+
<plugin>
47+
<groupId>org.jetbrains.kotlin</groupId>
48+
<artifactId>kotlin-maven-plugin</artifactId>
49+
<version>${kotlin.version}</version>
50+
<executions>
51+
<execution>
52+
<id>compile</id>
53+
<goals>
54+
<goal>compile</goal>
55+
</goals>
56+
</execution>
57+
<execution>
58+
<id>test-compile</id>
59+
<goals>
60+
<goal>test-compile</goal>
61+
</goals>
62+
</execution>
63+
</executions>
64+
<configuration>
65+
<jvmTarget>${maven.compiler.target}</jvmTarget>
66+
</configuration>
67+
</plugin>
68+
<plugin>
69+
<groupId>org.apache.maven.plugins</groupId>
70+
<artifactId>maven-compiler-plugin</artifactId>
71+
<executions>
72+
<!-- Replacing default-compile as it is treated specially by maven -->
73+
<execution>
74+
<id>default-compile</id>
75+
<phase>none</phase>
76+
</execution>
77+
<!-- Replacing default-testCompile as it is treated specially by maven -->
78+
<execution>
79+
<id>default-testCompile</id>
80+
<phase>none</phase>
81+
</execution>
82+
<execution>
83+
<id>java-compile</id>
84+
<phase>compile</phase>
85+
<goals>
86+
<goal>compile</goal>
87+
</goals>
88+
<configuration>
89+
<annotationProcessorPaths>
90+
<path>
91+
<groupId>io.quarkus</groupId>
92+
<artifactId>quarkus-extension-processor</artifactId>
93+
<version>${project.version}</version>
94+
</path>
95+
</annotationProcessorPaths>
96+
</configuration>
97+
</execution>
98+
<execution>
99+
<id>java-test-compile</id>
100+
<phase>test-compile</phase>
101+
<goals>
102+
<goal>testCompile</goal>
103+
</goals>
104+
</execution>
105+
</executions>
106+
</plugin>
107+
<plugin>
108+
<groupId>org.apache.maven.plugins</groupId>
109+
<artifactId>maven-javadoc-plugin</artifactId>
110+
<configuration>
111+
<failOnError>false</failOnError>
112+
</configuration>
113+
</plugin>
114+
</plugins>
115+
</build>
116+
117+
</project>
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package io.quarkus.arc.kotlin
2+
3+
import io.quarkus.arc.Arc
4+
import io.quarkus.arc.InjectableContext
5+
import io.quarkus.arc.ManagedContext
6+
import kotlin.coroutines.CoroutineContext
7+
import kotlin.coroutines.EmptyCoroutineContext
8+
import kotlinx.coroutines.CoroutineScope
9+
import kotlinx.coroutines.CoroutineStart
10+
import kotlinx.coroutines.Deferred
11+
import kotlinx.coroutines.ThreadContextElement
12+
import kotlinx.coroutines.async
13+
import kotlinx.coroutines.withContext
14+
15+
/**
16+
* A suspending function that executes a block of code within the Quarkus Request Context.
17+
*
18+
* This function captures the current request context and ensures it is activated when the coroutine
19+
* resumes on a thread.
20+
*
21+
* If the request context is finalized before the block completes, it results in undefined behavior.
22+
*
23+
* Will not start a request context if there is none active at the time of invocation.
24+
*
25+
* @param context The CoroutineContext to use for the coroutine.
26+
* @param block The block of code to execute within the request context.
27+
* @return The result of the block execution.
28+
*/
29+
suspend fun <T> withPropagatedContext(
30+
context: CoroutineContext,
31+
block: suspend CoroutineScope.() -> T,
32+
): T {
33+
return withContext(context = context.appendRequestContextToCoroutineContext(), block = block)
34+
}
35+
36+
/**
37+
* An async function that executes a block of code within the Quarkus Request Context.
38+
*
39+
* This function captures the current request context and ensures it is activated when the coroutine
40+
* resumes on a thread.
41+
*
42+
* If the caller finalizes the request context before the block is executed, results in undefined
43+
* behavior.
44+
*
45+
* Will not start a request context if there is none active at the time of invocation.
46+
*
47+
* @param context The CoroutineContext to use for the coroutine.
48+
* @param block The block of code to execute within the request context.
49+
*/
50+
fun <T> CoroutineScope.asyncWithPropagatedContext(
51+
context: CoroutineContext = EmptyCoroutineContext,
52+
start: CoroutineStart = CoroutineStart.DEFAULT,
53+
block: suspend CoroutineScope.() -> T,
54+
): Deferred<T> {
55+
return async(
56+
context = context.appendRequestContextToCoroutineContext(),
57+
start = start,
58+
block = block,
59+
)
60+
}
61+
62+
fun CoroutineContext.appendRequestContextToCoroutineContext(): CoroutineContext {
63+
val requestContext: ManagedContext? = Arc.container()?.requestContext()
64+
return if (requestContext == null) {
65+
this
66+
} else {
67+
this + RequestContextCoroutineContext(requestContext = requestContext)
68+
}
69+
}
70+
71+
/**
72+
* A CoroutineContext.Element to propagate the Quarkus Request Context.
73+
*
74+
* This element captures the active request context when a coroutine is launched and ensures it is
75+
* activated whenever the coroutine resumes on a thread.
76+
*
77+
* @param requestContext The Quarkus ManagedContext for the request scope.
78+
*/
79+
class RequestContextCoroutineContext(private val requestContext: ManagedContext) :
80+
ThreadContextElement<RequestContextCoroutineContext.ContextSnapshot> {
81+
82+
private val state: InjectableContext.ContextState? = requestContext.stateIfActive
83+
private val classLoader: ClassLoader = Thread.currentThread().contextClassLoader
84+
85+
fun InjectableContext.ContextState?.isNullOrInvalid(): Boolean {
86+
return this == null || !this.isValid
87+
}
88+
89+
/** A companion object to act as the Key for this context element. */
90+
companion object Key : CoroutineContext.Key<RequestContextCoroutineContext>
91+
92+
/** The key that identifies this element in a CoroutineContext. */
93+
override val key: CoroutineContext.Key<*>
94+
get() = Key
95+
96+
/**
97+
* This function is invoked when the coroutine resumes execution on a thread. It activates the
98+
* captured request context.
99+
*
100+
* @param context The coroutine context.
101+
* @return The state of the request context *before* this element activated its captured state.
102+
* This is used by `restoreThreadContext` to correctly reset the context later.
103+
*/
104+
override fun updateThreadContext(context: CoroutineContext): ContextSnapshot {
105+
// Capture the state of the current thread's context before we change it.
106+
val oldState = requestContext.stateIfActive
107+
108+
val oldClassLoader = Thread.currentThread().contextClassLoader
109+
110+
Thread.currentThread().contextClassLoader = classLoader
111+
112+
// If the coroutine was launched from a thread without an active request context,
113+
// we should deactivate any context that might be active on the current thread.
114+
if (state.isNullOrInvalid()) {
115+
requestContext.deactivate()
116+
} else {
117+
// Activate the request context that we captured when the coroutine was created.
118+
requestContext.activate(state)
119+
}
120+
121+
return ContextSnapshot(oldState, oldClassLoader)
122+
}
123+
124+
/**
125+
* This function is invoked when the coroutine suspends or completes. It restores the request
126+
* context of the thread to its original state.
127+
*
128+
* @param context The coroutine context.
129+
* @param oldState The state that was returned by `updateThreadContext`.
130+
*/
131+
override fun restoreThreadContext(context: CoroutineContext, oldState: ContextSnapshot) {
132+
133+
Thread.currentThread().contextClassLoader = oldState.classLoader
134+
135+
// We must restore the request context on the thread to whatever it was before
136+
// this coroutine resumed.
137+
val oldContext = oldState.contextState
138+
if (oldContext.isNullOrInvalid()) {
139+
requestContext.deactivate()
140+
} else {
141+
requestContext.activate(oldContext)
142+
}
143+
}
144+
145+
data class ContextSnapshot(
146+
val contextState: InjectableContext.ContextState? = null,
147+
val classLoader: ClassLoader,
148+
)
149+
}

extensions/arc/pom.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
<packaging>pom</packaging>
1616
<modules>
1717
<module>deployment</module>
18+
<module>kotlin</module>
1819
<module>runtime</module>
1920
<module>test-supplement</module>
2021
<module>test-supplement-decorator</module>
2122
<module>runtime-dev</module>
23+
<module>tests</module>
2224
</modules>
2325

2426
</project>

extensions/arc/runtime/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
<groupId>io.quarkus</groupId>
2323
<artifactId>quarkus-core</artifactId>
2424
</dependency>
25+
<dependency>
26+
<groupId>io.quarkus</groupId>
27+
<artifactId>quarkus-arc-kotlin</artifactId>
28+
</dependency>
2529
<dependency>
2630
<groupId>org.eclipse.microprofile.context-propagation</groupId>
2731
<artifactId>microprofile-context-propagation-api</artifactId>

0 commit comments

Comments
 (0)