From 94954e9b056abd3bc426af07f746bf2608484a9a Mon Sep 17 00:00:00 2001 From: Paulo Casaes Date: Tue, 22 Jul 2025 23:14:11 -0700 Subject: [PATCH 1/4] Add Kotlin Coroutines CDI Context Propagation --- bom/application/pom.xml | 5 + extensions/arc/deployment/pom.xml | 32 ++ .../RequestContextCoroutineContextTest.kt | 456 ++++++++++++++++++ extensions/arc/kotlin/pom.xml | 117 +++++ .../kotlin/RequestContextCoroutineContext.kt | 149 ++++++ extensions/arc/pom.xml | 1 + extensions/arc/runtime/pom.xml | 4 + 7 files changed, 764 insertions(+) create mode 100644 extensions/arc/deployment/src/test/kotlin/io/quarkus/arc/kotlin/RequestContextCoroutineContextTest.kt create mode 100644 extensions/arc/kotlin/pom.xml create mode 100644 extensions/arc/kotlin/src/main/kotlin/io/quarkus/arc/kotlin/RequestContextCoroutineContext.kt diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 5adbdfc83b201..ffadf2ad5e140 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -613,6 +613,11 @@ quarkus-arc ${project.version} + + io.quarkus + quarkus-arc-kotlin + ${project.version} + io.quarkus quarkus-arc-dev diff --git a/extensions/arc/deployment/pom.xml b/extensions/arc/deployment/pom.xml index e4760e556668b..54417676dcaf7 100644 --- a/extensions/arc/deployment/pom.xml +++ b/extensions/arc/deployment/pom.xml @@ -66,6 +66,11 @@ test + + org.jetbrains.kotlinx + kotlinx-coroutines-test + test + @@ -87,6 +92,33 @@ + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + + compile + + + + test-compile + + test-compile + + + + src/test/kotlin + + + + + + ${maven.compiler.target} + + diff --git a/extensions/arc/deployment/src/test/kotlin/io/quarkus/arc/kotlin/RequestContextCoroutineContextTest.kt b/extensions/arc/deployment/src/test/kotlin/io/quarkus/arc/kotlin/RequestContextCoroutineContextTest.kt new file mode 100644 index 0000000000000..bfdba269b8ec7 --- /dev/null +++ b/extensions/arc/deployment/src/test/kotlin/io/quarkus/arc/kotlin/RequestContextCoroutineContextTest.kt @@ -0,0 +1,456 @@ +package io.quarkus.arc.kotlin + +import io.quarkus.arc.Arc +import io.quarkus.test.QuarkusUnitTest +import jakarta.enterprise.context.RequestScoped +import jakarta.inject.Inject +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import org.jboss.shrinkwrap.api.spec.JavaArchive +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertNotNull +import org.junit.jupiter.api.extension.RegisterExtension + +class RequestContextCoroutineContextTest { + + companion object { + @RegisterExtension + @JvmStatic + val TEST = + QuarkusUnitTest().withApplicationRoot { jar: JavaArchive -> + jar.addClasses(RequestData::class.java) + } + } + + @Inject lateinit var requestData: RequestData + + lateinit var expectedClassLoader: ClassLoader + + @BeforeEach + @AfterEach + fun setUp() { + if (Arc.container().requestContext().isActive) { + Arc.container().requestContext().terminate() + } + this.expectedClassLoader = Thread.currentThread().contextClassLoader + } + + private fun assertThatCallersClassLoaderIsExpected() { + Assertions.assertEquals( + expectedClassLoader, + Thread.currentThread().contextClassLoader, + "Thread context class loader should be the expected one", + ) + } + + @Test + fun `without an active request scope on withContext`() { + // GIVEN no active request context + Assertions.assertFalse( + Arc.container().requestContext().isActive, + "Request context should not be active", + ) + + // WHEN we run a block + runTest { + withPropagatedContext(Dispatchers.IO) { + // THEN the request context should not be active + Assertions.assertFalse( + Arc.container().requestContext().isActive, + "Request context should not be active", + ) + + assertThatCallersClassLoaderIsExpected() + } + } + } + + @Test + fun `without an active request scope on async`() { + // GIVEN no active request context + Assertions.assertFalse( + Arc.container().requestContext().isActive, + "Request context should not be active", + ) + + // WHEN we run a block with async + runTest { + coroutineScope { + asyncWithPropagatedContext(Dispatchers.IO) { + // THEN the request context should not be active + Assertions.assertFalse( + Arc.container().requestContext().isActive, + "Request context should not be active", + ) + + assertThatCallersClassLoaderIsExpected() + } + .await() + } + } + } + + @Test + fun `with an active request scope on withContext`() { + // GIVEN an active request context + Arc.container().requestContext().activate() + Assertions.assertTrue( + Arc.container().requestContext().isActive, + "Request context should be active", + ) + + // AND a given number and an expected post-async number + val givenNumber = 1234L + val expectedPostAsyncNumber = 5432L + + // AND we set the number value in the request data + requestData.numberValue = givenNumber + + // WHEN we run a block with the request context + runTest { + withPropagatedContext(Dispatchers.IO) { + // THEN the request context should be active + Assertions.assertTrue( + Arc.container().requestContext().isActive, + "Request context should be active", + ) + + // AND the number value should match the given number + Assertions.assertEquals(givenNumber, requestData.numberValue) + + assertThatCallersClassLoaderIsExpected() + + // AND the number value should match the given number after a short delay + delay(10) + Assertions.assertEquals(givenNumber, requestData.numberValue) + + assertThatCallersClassLoaderIsExpected() + + // WHEN we set the number value to the expected post-async number + requestData.numberValue = expectedPostAsyncNumber + + // THEN the number value should match the expected post-async number after a short + // delay + delay(10) + Assertions.assertEquals(expectedPostAsyncNumber, requestData.numberValue) + + assertThatCallersClassLoaderIsExpected() + } + } + + // THEN the number value should match the expected post-async number after the block + // execution + Assertions.assertEquals(expectedPostAsyncNumber, requestData.numberValue) + } + + @Test + fun `with an active request scope on async`() { + // GIVEN an active request context + Arc.container().requestContext().activate() + Assertions.assertTrue( + Arc.container().requestContext().isActive, + "Request context should be active", + ) + + // AND a given number and an expected post-async number + val givenNumber = 1234L + val expectedPostAsyncNumber = 5432L + + // AND we set the number value in the request data + requestData.numberValue = givenNumber + + // WHEN we run a block with async + runTest { + coroutineScope { + asyncWithPropagatedContext(Dispatchers.IO) { + // THEN the request context should be active + Assertions.assertTrue( + Arc.container().requestContext().isActive, + "Request context should be active", + ) + + // AND the number value should match the given number + Assertions.assertEquals(givenNumber, requestData.numberValue) + + assertThatCallersClassLoaderIsExpected() + + // AND the number value should match the given number after a short delay + delay(10) + Assertions.assertEquals(givenNumber, requestData.numberValue) + + assertThatCallersClassLoaderIsExpected() + + // WHEN we set the number value to the expected post-async number + requestData.numberValue = expectedPostAsyncNumber + + // THEN the number value should match the expected post-async number after a + // short delay + delay(10) + Assertions.assertEquals(expectedPostAsyncNumber, requestData.numberValue) + + assertThatCallersClassLoaderIsExpected() + } + .await() + } + } + + // THEN the number value should match the expected post-async number after the block + // execution + Assertions.assertEquals(expectedPostAsyncNumber, requestData.numberValue) + } + + @Test + fun `with an active request scope on inner coroutine scope in async`() { + // GIVEN an active request context + Arc.container().requestContext().activate() + Assertions.assertTrue( + Arc.container().requestContext().isActive, + "Request context should be active", + ) + + // AND a given number and an expected post-async number + val givenNumber = 1234L + val expectedPostAsyncNumber = 5432L + + // AND we set the number value in the request data + requestData.numberValue = givenNumber + + // WHEN we run a block with async + runTest { + coroutineScope { + asyncWithPropagatedContext(Dispatchers.IO) { + coroutineScope { + // THEN the request context should be active + Assertions.assertTrue( + Arc.container().requestContext().isActive, + "Request context should be active", + ) + + // AND the number value should match the given number + Assertions.assertEquals(givenNumber, requestData.numberValue) + + assertThatCallersClassLoaderIsExpected() + + // AND the number value should match the given number after a short + // delay + delay(10) + Assertions.assertEquals(givenNumber, requestData.numberValue) + + assertThatCallersClassLoaderIsExpected() + + // WHEN we set the number value to the expected post-async number + requestData.numberValue = expectedPostAsyncNumber + + // THEN the number value should match the expected post-async number + // after a short delay + delay(10) + Assertions.assertEquals( + expectedPostAsyncNumber, + requestData.numberValue, + ) + + assertThatCallersClassLoaderIsExpected() + } + } + .await() + } + } + + // THEN the number value should match the expected post-async number after the block + // execution + Assertions.assertEquals(expectedPostAsyncNumber, requestData.numberValue) + } + + @Test + fun `with a terminated request scope while on async (undefined behavior)`() { + // GIVEN an active request context + Arc.container().requestContext().activate() + Assertions.assertTrue( + Arc.container().requestContext().isActive, + "Request context should be active", + ) + + // AND a given number + val givenNumber = 1234L + + // AND we set the number value in the request data + requestData.numberValue = givenNumber + + val asyncStarted = CompletableDeferred() + val requestScopeTerminated = CompletableDeferred() + + // WHEN we run a block with async + runTest { + val job = launch { + asyncWithPropagatedContext(Dispatchers.IO) { + // THEN the request context should be active + Assertions.assertTrue( + Arc.container().requestContext().isActive, + "Request context should be active", + ) + + // AND the number value should match the given number + Assertions.assertEquals(givenNumber, requestData.numberValue) + + assertThatCallersClassLoaderIsExpected() + + asyncStarted.complete(Unit) + + // WHEN we wait for the request context to be terminated by another thread + requestScopeTerminated.await() + + // THEN the request context should not be active (undefined behavior) + Assertions.assertFalse( + Arc.container().requestContext().isActive, + "Request context should not be active", + ) + } + .await() + } + + launch { + asyncStarted.await() + + Assertions.assertTrue( + Arc.container().requestContext().isActive, + "Request context should be active", + ) + Assertions.assertEquals(givenNumber, requestData.numberValue) + Arc.container().requestContext().terminate() + Assertions.assertFalse( + Arc.container().requestContext().isActive, + "Request context should be active", + ) + + requestScopeTerminated.complete(Unit) + } + + job.join() + } + } + + @Test + fun `with two active request scope on async on same coroutine`() { + // GIVEN two active request contexts with different giveNumbers + Arc.container().requestContext().activate() + + val firstRequestState = Arc.container().requestContext().stateIfActive + assertNotNull(firstRequestState) + + val firstGivenNumber = 1234L + val expectedFirstGivenNumber = 91234L + requestData.numberValue = firstGivenNumber + + Arc.container().requestContext().activate() + + val secondRequestState = Arc.container().requestContext().stateIfActive + assertNotNull(secondRequestState) + Assertions.assertNotEquals(firstGivenNumber, requestData.numberValue) + + val secondGivenNumber = 5432L + val expectedSecondGivenNumber = 95432L + requestData.numberValue = secondGivenNumber + + Assertions.assertNotEquals(firstRequestState, secondRequestState) + + val waitForFirstEnd = CompletableDeferred() + val waitForSecondMiddle = CompletableDeferred() + val waitForSecondEnd = CompletableDeferred() + + // WHEN we run a block with async + runTest { + val jobFirstRequest = launch { + Arc.container().requestContext().activate(firstRequestState) + asyncWithPropagatedContext(Dispatchers.IO) { + // THEN the request context should be active + Assertions.assertTrue( + Arc.container().requestContext().isActive, + "Request context should be active", + ) + + // AND the number value should match the given number + Assertions.assertEquals(firstGivenNumber, requestData.numberValue) + + assertThatCallersClassLoaderIsExpected() + + waitForSecondMiddle.await() + + // THEN the request context should be active + Assertions.assertTrue( + Arc.container().requestContext().isActive, + "Request context should be active", + ) + + // AND the number value should match the given number + Assertions.assertEquals(firstGivenNumber, requestData.numberValue) + + assertThatCallersClassLoaderIsExpected() + + requestData.numberValue = expectedFirstGivenNumber + + waitForFirstEnd.complete(Unit) + } + .await() + } + + val jobSecondRequest = launch { + Arc.container().requestContext().activate(secondRequestState) + asyncWithPropagatedContext(Dispatchers.IO) { + // THEN the request context should be active + Assertions.assertTrue( + Arc.container().requestContext().isActive, + "Request context should be active", + ) + + // AND the number value should match the given number + Assertions.assertEquals(secondGivenNumber, requestData.numberValue) + + assertThatCallersClassLoaderIsExpected() + + waitForSecondMiddle.complete(Unit) + + waitForFirstEnd.await() + + // THEN the request context should be active + Assertions.assertTrue( + Arc.container().requestContext().isActive, + "Request context should be active", + ) + + // AND the number value should match the given number + Assertions.assertEquals(secondGivenNumber, requestData.numberValue) + + assertThatCallersClassLoaderIsExpected() + + requestData.numberValue = expectedSecondGivenNumber + + waitForSecondEnd.complete(Unit) + } + .await() + } + + jobSecondRequest.join() + jobFirstRequest.join() + } + + Arc.container().requestContext().activate(secondRequestState) + Assertions.assertEquals(expectedSecondGivenNumber, requestData.numberValue) + Arc.container().requestContext().terminate() + + Arc.container().requestContext().activate(firstRequestState) + Assertions.assertEquals(expectedFirstGivenNumber, requestData.numberValue) + Arc.container().requestContext().terminate() + } + + @RequestScoped + class RequestData { + var numberValue = 0L + } +} diff --git a/extensions/arc/kotlin/pom.xml b/extensions/arc/kotlin/pom.xml new file mode 100644 index 0000000000000..71b6892f7712e --- /dev/null +++ b/extensions/arc/kotlin/pom.xml @@ -0,0 +1,117 @@ + + + 4.0.0 + + io.quarkus + quarkus-arc-parent + 999-SNAPSHOT + + + quarkus-arc-kotlin + Quarkus - Arc - Kotlin + + + + io.quarkus.arc + arc + + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + true + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + + + org.jetbrains.kotlinx + kotlinx-coroutines-core-jvm + + + org.jetbrains.kotlinx + kotlinx-coroutines-jdk8 + + + + + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/test/kotlin + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + + compile + + + + test-compile + + test-compile + + + + + ${maven.compiler.target} + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + default-compile + none + + + + default-testCompile + none + + + java-compile + compile + + compile + + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + java-test-compile + test-compile + + testCompile + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + false + + + + + + \ No newline at end of file diff --git a/extensions/arc/kotlin/src/main/kotlin/io/quarkus/arc/kotlin/RequestContextCoroutineContext.kt b/extensions/arc/kotlin/src/main/kotlin/io/quarkus/arc/kotlin/RequestContextCoroutineContext.kt new file mode 100644 index 0000000000000..f9957e528a0ec --- /dev/null +++ b/extensions/arc/kotlin/src/main/kotlin/io/quarkus/arc/kotlin/RequestContextCoroutineContext.kt @@ -0,0 +1,149 @@ +package io.quarkus.arc.kotlin + +import io.quarkus.arc.Arc +import io.quarkus.arc.InjectableContext +import io.quarkus.arc.ManagedContext +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.ThreadContextElement +import kotlinx.coroutines.async +import kotlinx.coroutines.withContext + +/** + * A suspending function that executes a block of code within the Quarkus Request Context. + * + * This function captures the current request context and ensures it is activated when the coroutine + * resumes on a thread. + * + * If the request context is finalized before the block completes, it results in undefined behavior. + * + * Will not start a request context if there is none active at the time of invocation. + * + * @param context The CoroutineContext to use for the coroutine. + * @param block The block of code to execute within the request context. + * @return The result of the block execution. + */ +suspend fun withPropagatedContext( + context: CoroutineContext, + block: suspend CoroutineScope.() -> T, +): T { + return withContext(context = context.appendRequestContextToCoroutineContext(), block = block) +} + +/** + * An async function that executes a block of code within the Quarkus Request Context. + * + * This function captures the current request context and ensures it is activated when the coroutine + * resumes on a thread. + * + * If the caller finalizes the request context before the block is executed, results in undefined + * behavior. + * + * Will not start a request context if there is none active at the time of invocation. + * + * @param context The CoroutineContext to use for the coroutine. + * @param block The block of code to execute within the request context. + */ +fun CoroutineScope.asyncWithPropagatedContext( + context: CoroutineContext = EmptyCoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + block: suspend CoroutineScope.() -> T, +): Deferred { + return async( + context = context.appendRequestContextToCoroutineContext(), + start = start, + block = block, + ) +} + +fun CoroutineContext.appendRequestContextToCoroutineContext(): CoroutineContext { + val requestContext: ManagedContext? = Arc.container()?.requestContext() + return if (requestContext == null) { + this + } else { + this + RequestContextCoroutineContext(requestContext = requestContext) + } +} + +/** + * A CoroutineContext.Element to propagate the Quarkus Request Context. + * + * This element captures the active request context when a coroutine is launched and ensures it is + * activated whenever the coroutine resumes on a thread. + * + * @param requestContext The Quarkus ManagedContext for the request scope. + */ +class RequestContextCoroutineContext(private val requestContext: ManagedContext) : + ThreadContextElement { + + private val state: InjectableContext.ContextState? = requestContext.stateIfActive + private val classLoader: ClassLoader = Thread.currentThread().contextClassLoader + + fun InjectableContext.ContextState?.isNullOrInvalid(): Boolean { + return this == null || !this.isValid + } + + /** A companion object to act as the Key for this context element. */ + companion object Key : CoroutineContext.Key + + /** The key that identifies this element in a CoroutineContext. */ + override val key: CoroutineContext.Key<*> + get() = Key + + /** + * This function is invoked when the coroutine resumes execution on a thread. It activates the + * captured request context. + * + * @param context The coroutine context. + * @return The state of the request context *before* this element activated its captured state. + * This is used by `restoreThreadContext` to correctly reset the context later. + */ + override fun updateThreadContext(context: CoroutineContext): ContextSnapshot { + // Capture the state of the current thread's context before we change it. + val oldState = requestContext.stateIfActive + + val oldClassLoader = Thread.currentThread().contextClassLoader + + Thread.currentThread().contextClassLoader = classLoader + + // If the coroutine was launched from a thread without an active request context, + // we should deactivate any context that might be active on the current thread. + if (state.isNullOrInvalid()) { + requestContext.deactivate() + } else { + // Activate the request context that we captured when the coroutine was created. + requestContext.activate(state) + } + + return ContextSnapshot(oldState, oldClassLoader) + } + + /** + * This function is invoked when the coroutine suspends or completes. It restores the request + * context of the thread to its original state. + * + * @param context The coroutine context. + * @param oldState The state that was returned by `updateThreadContext`. + */ + override fun restoreThreadContext(context: CoroutineContext, oldState: ContextSnapshot) { + + Thread.currentThread().contextClassLoader = oldState.classLoader + + // We must restore the request context on the thread to whatever it was before + // this coroutine resumed. + val oldContext = oldState.contextState + if (oldContext.isNullOrInvalid()) { + requestContext.deactivate() + } else { + requestContext.activate(oldContext) + } + } + + data class ContextSnapshot( + val contextState: InjectableContext.ContextState? = null, + val classLoader: ClassLoader, + ) +} diff --git a/extensions/arc/pom.xml b/extensions/arc/pom.xml index bc7791e4327a3..95ae6b5b9a7c2 100644 --- a/extensions/arc/pom.xml +++ b/extensions/arc/pom.xml @@ -15,6 +15,7 @@ pom deployment + kotlin runtime test-supplement test-supplement-decorator diff --git a/extensions/arc/runtime/pom.xml b/extensions/arc/runtime/pom.xml index 1c618b1b97645..8bb09a71e85ca 100644 --- a/extensions/arc/runtime/pom.xml +++ b/extensions/arc/runtime/pom.xml @@ -22,6 +22,10 @@ io.quarkus quarkus-core + + io.quarkus + quarkus-arc-kotlin + org.eclipse.microprofile.context-propagation microprofile-context-propagation-api From a65131979b14351fb371569f073b18c4df3806ff Mon Sep 17 00:00:00 2001 From: Paulo Casaes Date: Wed, 23 Jul 2025 08:10:45 -0700 Subject: [PATCH 2/4] Simplify API --- .../RequestContextCoroutineContextTest.kt | 18 ++++--- .../kotlin/RequestContextCoroutineContext.kt | 52 ++----------------- 2 files changed, 14 insertions(+), 56 deletions(-) diff --git a/extensions/arc/deployment/src/test/kotlin/io/quarkus/arc/kotlin/RequestContextCoroutineContextTest.kt b/extensions/arc/deployment/src/test/kotlin/io/quarkus/arc/kotlin/RequestContextCoroutineContextTest.kt index bfdba269b8ec7..2529543122ead 100644 --- a/extensions/arc/deployment/src/test/kotlin/io/quarkus/arc/kotlin/RequestContextCoroutineContextTest.kt +++ b/extensions/arc/deployment/src/test/kotlin/io/quarkus/arc/kotlin/RequestContextCoroutineContextTest.kt @@ -6,10 +6,12 @@ import jakarta.enterprise.context.RequestScoped import jakarta.inject.Inject import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext import org.jboss.shrinkwrap.api.spec.JavaArchive import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions @@ -60,7 +62,7 @@ class RequestContextCoroutineContextTest { // WHEN we run a block runTest { - withPropagatedContext(Dispatchers.IO) { + withContext(Dispatchers.IO.withCdiContext()) { // THEN the request context should not be active Assertions.assertFalse( Arc.container().requestContext().isActive, @@ -83,7 +85,7 @@ class RequestContextCoroutineContextTest { // WHEN we run a block with async runTest { coroutineScope { - asyncWithPropagatedContext(Dispatchers.IO) { + async(Dispatchers.IO.withCdiContext()) { // THEN the request context should not be active Assertions.assertFalse( Arc.container().requestContext().isActive, @@ -115,7 +117,7 @@ class RequestContextCoroutineContextTest { // WHEN we run a block with the request context runTest { - withPropagatedContext(Dispatchers.IO) { + withContext(Dispatchers.IO.withCdiContext()) { // THEN the request context should be active Assertions.assertTrue( Arc.container().requestContext().isActive, @@ -169,7 +171,7 @@ class RequestContextCoroutineContextTest { // WHEN we run a block with async runTest { coroutineScope { - asyncWithPropagatedContext(Dispatchers.IO) { + async(Dispatchers.IO.withCdiContext()) { // THEN the request context should be active Assertions.assertTrue( Arc.container().requestContext().isActive, @@ -225,7 +227,7 @@ class RequestContextCoroutineContextTest { // WHEN we run a block with async runTest { coroutineScope { - asyncWithPropagatedContext(Dispatchers.IO) { + async(Dispatchers.IO.withCdiContext()) { coroutineScope { // THEN the request context should be active Assertions.assertTrue( @@ -289,7 +291,7 @@ class RequestContextCoroutineContextTest { // WHEN we run a block with async runTest { val job = launch { - asyncWithPropagatedContext(Dispatchers.IO) { + async(Dispatchers.IO.withCdiContext()) { // THEN the request context should be active Assertions.assertTrue( Arc.container().requestContext().isActive, @@ -368,7 +370,7 @@ class RequestContextCoroutineContextTest { runTest { val jobFirstRequest = launch { Arc.container().requestContext().activate(firstRequestState) - asyncWithPropagatedContext(Dispatchers.IO) { + async(Dispatchers.IO.withCdiContext()) { // THEN the request context should be active Assertions.assertTrue( Arc.container().requestContext().isActive, @@ -402,7 +404,7 @@ class RequestContextCoroutineContextTest { val jobSecondRequest = launch { Arc.container().requestContext().activate(secondRequestState) - asyncWithPropagatedContext(Dispatchers.IO) { + async(Dispatchers.IO.withCdiContext()) { // THEN the request context should be active Assertions.assertTrue( Arc.container().requestContext().isActive, diff --git a/extensions/arc/kotlin/src/main/kotlin/io/quarkus/arc/kotlin/RequestContextCoroutineContext.kt b/extensions/arc/kotlin/src/main/kotlin/io/quarkus/arc/kotlin/RequestContextCoroutineContext.kt index f9957e528a0ec..7a3ea8d4447c9 100644 --- a/extensions/arc/kotlin/src/main/kotlin/io/quarkus/arc/kotlin/RequestContextCoroutineContext.kt +++ b/extensions/arc/kotlin/src/main/kotlin/io/quarkus/arc/kotlin/RequestContextCoroutineContext.kt @@ -4,62 +4,18 @@ import io.quarkus.arc.Arc import io.quarkus.arc.InjectableContext import io.quarkus.arc.ManagedContext import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Deferred import kotlinx.coroutines.ThreadContextElement -import kotlinx.coroutines.async -import kotlinx.coroutines.withContext /** - * A suspending function that executes a block of code within the Quarkus Request Context. + * This function extends the CoroutineContext to include the Quarkus Request Context if it is + * active. * - * This function captures the current request context and ensures it is activated when the coroutine - * resumes on a thread. - * - * If the request context is finalized before the block completes, it results in undefined behavior. - * - * Will not start a request context if there is none active at the time of invocation. - * - * @param context The CoroutineContext to use for the coroutine. - * @param block The block of code to execute within the request context. - * @return The result of the block execution. - */ -suspend fun withPropagatedContext( - context: CoroutineContext, - block: suspend CoroutineScope.() -> T, -): T { - return withContext(context = context.appendRequestContextToCoroutineContext(), block = block) -} - -/** - * An async function that executes a block of code within the Quarkus Request Context. - * - * This function captures the current request context and ensures it is activated when the coroutine - * resumes on a thread. - * - * If the caller finalizes the request context before the block is executed, results in undefined + * If the caller finalizes the request context before the coroutine resumes, it results in undefined * behavior. * * Will not start a request context if there is none active at the time of invocation. - * - * @param context The CoroutineContext to use for the coroutine. - * @param block The block of code to execute within the request context. */ -fun CoroutineScope.asyncWithPropagatedContext( - context: CoroutineContext = EmptyCoroutineContext, - start: CoroutineStart = CoroutineStart.DEFAULT, - block: suspend CoroutineScope.() -> T, -): Deferred { - return async( - context = context.appendRequestContextToCoroutineContext(), - start = start, - block = block, - ) -} - -fun CoroutineContext.appendRequestContextToCoroutineContext(): CoroutineContext { +fun CoroutineContext.withCdiContext(): CoroutineContext { val requestContext: ManagedContext? = Arc.container()?.requestContext() return if (requestContext == null) { this From 6a4b16b57d5c2a9549139c861d6b844605803e2a Mon Sep 17 00:00:00 2001 From: Paulo Casaes Date: Wed, 23 Jul 2025 12:17:01 -0700 Subject: [PATCH 3/4] Move RequestContextCoroutineContext to kotlin module --- bom/application/pom.xml | 5 - extensions/arc/deployment/pom.xml | 32 ----- extensions/arc/kotlin/pom.xml | 117 ------------------ extensions/arc/pom.xml | 1 - extensions/arc/runtime/pom.xml | 4 - extensions/kotlin/deployment/pom.xml | 51 ++++++++ .../RequestContextCoroutineContextTest.kt | 2 +- extensions/kotlin/runtime/pom.xml | 24 ++++ .../arc}/RequestContextCoroutineContext.kt | 2 +- 9 files changed, 77 insertions(+), 161 deletions(-) delete mode 100644 extensions/arc/kotlin/pom.xml rename extensions/{arc/deployment/src/test/kotlin/io/quarkus/arc/kotlin => kotlin/deployment/src/test/kotlin/io/quarkus/kotlin/arc}/RequestContextCoroutineContextTest.kt (99%) rename extensions/{arc/kotlin/src/main/kotlin/io/quarkus/arc/kotlin => kotlin/runtime/src/main/kotlin/io/quarkus/kotlin/arc}/RequestContextCoroutineContext.kt (99%) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index ffadf2ad5e140..5adbdfc83b201 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -613,11 +613,6 @@ quarkus-arc ${project.version} - - io.quarkus - quarkus-arc-kotlin - ${project.version} - io.quarkus quarkus-arc-dev diff --git a/extensions/arc/deployment/pom.xml b/extensions/arc/deployment/pom.xml index 54417676dcaf7..e4760e556668b 100644 --- a/extensions/arc/deployment/pom.xml +++ b/extensions/arc/deployment/pom.xml @@ -66,11 +66,6 @@ test - - org.jetbrains.kotlinx - kotlinx-coroutines-test - test - @@ -92,33 +87,6 @@ - - org.jetbrains.kotlin - kotlin-maven-plugin - ${kotlin.version} - - - compile - - compile - - - - test-compile - - test-compile - - - - src/test/kotlin - - - - - - ${maven.compiler.target} - - diff --git a/extensions/arc/kotlin/pom.xml b/extensions/arc/kotlin/pom.xml deleted file mode 100644 index 71b6892f7712e..0000000000000 --- a/extensions/arc/kotlin/pom.xml +++ /dev/null @@ -1,117 +0,0 @@ - - - 4.0.0 - - io.quarkus - quarkus-arc-parent - 999-SNAPSHOT - - - quarkus-arc-kotlin - Quarkus - Arc - Kotlin - - - - io.quarkus.arc - arc - - - - org.jetbrains.kotlin - kotlin-stdlib-jdk8 - true - - - org.jetbrains.kotlinx - kotlinx-coroutines-core - - - org.jetbrains.kotlinx - kotlinx-coroutines-core-jvm - - - org.jetbrains.kotlinx - kotlinx-coroutines-jdk8 - - - - - - - ${project.basedir}/src/main/kotlin - ${project.basedir}/src/test/kotlin - - - org.jetbrains.kotlin - kotlin-maven-plugin - ${kotlin.version} - - - compile - - compile - - - - test-compile - - test-compile - - - - - ${maven.compiler.target} - - - - org.apache.maven.plugins - maven-compiler-plugin - - - - default-compile - none - - - - default-testCompile - none - - - java-compile - compile - - compile - - - - - io.quarkus - quarkus-extension-processor - ${project.version} - - - - - - java-test-compile - test-compile - - testCompile - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - false - - - - - - \ No newline at end of file diff --git a/extensions/arc/pom.xml b/extensions/arc/pom.xml index 95ae6b5b9a7c2..bc7791e4327a3 100644 --- a/extensions/arc/pom.xml +++ b/extensions/arc/pom.xml @@ -15,7 +15,6 @@ pom deployment - kotlin runtime test-supplement test-supplement-decorator diff --git a/extensions/arc/runtime/pom.xml b/extensions/arc/runtime/pom.xml index 8bb09a71e85ca..1c618b1b97645 100644 --- a/extensions/arc/runtime/pom.xml +++ b/extensions/arc/runtime/pom.xml @@ -22,10 +22,6 @@ io.quarkus quarkus-core - - io.quarkus - quarkus-arc-kotlin - org.eclipse.microprofile.context-propagation microprofile-context-propagation-api diff --git a/extensions/kotlin/deployment/pom.xml b/extensions/kotlin/deployment/pom.xml index dbd398037a393..1f73426ae3768 100644 --- a/extensions/kotlin/deployment/pom.xml +++ b/extensions/kotlin/deployment/pom.xml @@ -33,6 +33,30 @@ quarkus-vertx-kotlin-deployment true + + + io.quarkus + quarkus-junit5-internal + test + + + + io.quarkus + quarkus-arc-test-supplement + test + + + + io.quarkus + quarkus-arc-test-supplement-decorator + test + + + + org.jetbrains.kotlinx + kotlinx-coroutines-test + test + @@ -54,6 +78,33 @@ + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + + compile + + + + test-compile + + test-compile + + + + src/test/kotlin + + + + + + ${maven.compiler.target} + + diff --git a/extensions/arc/deployment/src/test/kotlin/io/quarkus/arc/kotlin/RequestContextCoroutineContextTest.kt b/extensions/kotlin/deployment/src/test/kotlin/io/quarkus/kotlin/arc/RequestContextCoroutineContextTest.kt similarity index 99% rename from extensions/arc/deployment/src/test/kotlin/io/quarkus/arc/kotlin/RequestContextCoroutineContextTest.kt rename to extensions/kotlin/deployment/src/test/kotlin/io/quarkus/kotlin/arc/RequestContextCoroutineContextTest.kt index 2529543122ead..564f4fc7641bb 100644 --- a/extensions/arc/deployment/src/test/kotlin/io/quarkus/arc/kotlin/RequestContextCoroutineContextTest.kt +++ b/extensions/kotlin/deployment/src/test/kotlin/io/quarkus/kotlin/arc/RequestContextCoroutineContextTest.kt @@ -1,4 +1,4 @@ -package io.quarkus.arc.kotlin +package io.quarkus.kotlin.arc import io.quarkus.arc.Arc import io.quarkus.test.QuarkusUnitTest diff --git a/extensions/kotlin/runtime/pom.xml b/extensions/kotlin/runtime/pom.xml index 7af7abdf3c66c..f95b671e572a8 100644 --- a/extensions/kotlin/runtime/pom.xml +++ b/extensions/kotlin/runtime/pom.xml @@ -14,6 +14,8 @@ Quarkus - Kotlin - Runtime Write your services in Kotlin + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/test/kotlin io.quarkus @@ -30,6 +32,28 @@ + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + + compile + + + + test-compile + + test-compile + + + + + ${maven.compiler.target} + + diff --git a/extensions/arc/kotlin/src/main/kotlin/io/quarkus/arc/kotlin/RequestContextCoroutineContext.kt b/extensions/kotlin/runtime/src/main/kotlin/io/quarkus/kotlin/arc/RequestContextCoroutineContext.kt similarity index 99% rename from extensions/arc/kotlin/src/main/kotlin/io/quarkus/arc/kotlin/RequestContextCoroutineContext.kt rename to extensions/kotlin/runtime/src/main/kotlin/io/quarkus/kotlin/arc/RequestContextCoroutineContext.kt index 7a3ea8d4447c9..76f77a7d6fbe9 100644 --- a/extensions/arc/kotlin/src/main/kotlin/io/quarkus/arc/kotlin/RequestContextCoroutineContext.kt +++ b/extensions/kotlin/runtime/src/main/kotlin/io/quarkus/kotlin/arc/RequestContextCoroutineContext.kt @@ -1,4 +1,4 @@ -package io.quarkus.arc.kotlin +package io.quarkus.kotlin.arc import io.quarkus.arc.Arc import io.quarkus.arc.InjectableContext From 63bc71d602408807950e9279ad28e3b9ed33f9b1 Mon Sep 17 00:00:00 2001 From: Paulo Casaes Date: Thu, 24 Jul 2025 08:06:37 -0700 Subject: [PATCH 4/4] Add runBlocking test --- .../arc/RequestContextCoroutineContextTest.kt | 359 ++++++++++-------- 1 file changed, 209 insertions(+), 150 deletions(-) diff --git a/extensions/kotlin/deployment/src/test/kotlin/io/quarkus/kotlin/arc/RequestContextCoroutineContextTest.kt b/extensions/kotlin/deployment/src/test/kotlin/io/quarkus/kotlin/arc/RequestContextCoroutineContextTest.kt index 564f4fc7641bb..9f05a9afb6f8a 100644 --- a/extensions/kotlin/deployment/src/test/kotlin/io/quarkus/kotlin/arc/RequestContextCoroutineContextTest.kt +++ b/extensions/kotlin/deployment/src/test/kotlin/io/quarkus/kotlin/arc/RequestContextCoroutineContextTest.kt @@ -3,6 +3,7 @@ package io.quarkus.kotlin.arc import io.quarkus.arc.Arc import io.quarkus.test.QuarkusUnitTest import jakarta.enterprise.context.RequestScoped +import jakarta.enterprise.context.control.ActivateRequestContext import jakarta.inject.Inject import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers @@ -10,15 +11,20 @@ import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withContext import org.jboss.shrinkwrap.api.spec.JavaArchive import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertNotNull import org.junit.jupiter.api.extension.RegisterExtension +import kotlin.coroutines.EmptyCoroutineContext class RequestContextCoroutineContextTest { @@ -31,31 +37,84 @@ class RequestContextCoroutineContextTest { } } - @Inject lateinit var requestData: RequestData + @Inject + lateinit var requestData: RequestData lateinit var expectedClassLoader: ClassLoader @BeforeEach @AfterEach fun setUp() { - if (Arc.container().requestContext().isActive) { - Arc.container().requestContext().terminate() - } this.expectedClassLoader = Thread.currentThread().contextClassLoader } private fun assertThatCallersClassLoaderIsExpected() { - Assertions.assertEquals( + assertEquals( expectedClassLoader, Thread.currentThread().contextClassLoader, "Thread context class loader should be the expected one", ) } + @Test + @ActivateRequestContext + fun `caller with active scope maintained in runBlocking`() = runBlocking { + assertTrue( + Arc.container().requestContext().isActive, + "Request context should be active", + ) + } + + @Test + @ActivateRequestContext + fun `runBlocking with active request`() { + // GIVEN an active request context + assertTrue( + Arc.container().requestContext().isActive, + "Request context should be active", + ) + + // AND a given number and an expected post-async number + val givenNumber = 1234L + val expectedPostAsyncNumber = 5432L + + // AND we set the number value in the request data + requestData.numberValue = givenNumber + + // WHEN we run a block with the request context + + runBlocking(context = EmptyCoroutineContext.withCdiContext()) { + assertTrue( + Arc.container().requestContext().isActive, + "Request context should be active", + ) + + // AND the number value should match the given number + assertEquals(givenNumber, requestData.numberValue) + + assertThatCallersClassLoaderIsExpected() + + // WHEN we set the number value to the expected post-async number + requestData.numberValue = expectedPostAsyncNumber + } + + // THEN the number value should match the expected post-async number after the block + // execution + assertEquals(expectedPostAsyncNumber, requestData.numberValue) + } + + @Test + fun `caller without active scope maintained in runBlocking`() = runBlocking { + assertFalse( + Arc.container().requestContext().isActive, + "Request context should not be active", + ) + } + @Test fun `without an active request scope on withContext`() { // GIVEN no active request context - Assertions.assertFalse( + assertFalse( Arc.container().requestContext().isActive, "Request context should not be active", ) @@ -64,7 +123,7 @@ class RequestContextCoroutineContextTest { runTest { withContext(Dispatchers.IO.withCdiContext()) { // THEN the request context should not be active - Assertions.assertFalse( + assertFalse( Arc.container().requestContext().isActive, "Request context should not be active", ) @@ -77,7 +136,7 @@ class RequestContextCoroutineContextTest { @Test fun `without an active request scope on async`() { // GIVEN no active request context - Assertions.assertFalse( + assertFalse( Arc.container().requestContext().isActive, "Request context should not be active", ) @@ -86,24 +145,24 @@ class RequestContextCoroutineContextTest { runTest { coroutineScope { async(Dispatchers.IO.withCdiContext()) { - // THEN the request context should not be active - Assertions.assertFalse( - Arc.container().requestContext().isActive, - "Request context should not be active", - ) - - assertThatCallersClassLoaderIsExpected() - } + // THEN the request context should not be active + assertFalse( + Arc.container().requestContext().isActive, + "Request context should not be active", + ) + + assertThatCallersClassLoaderIsExpected() + } .await() } } } @Test + @ActivateRequestContext fun `with an active request scope on withContext`() { // GIVEN an active request context - Arc.container().requestContext().activate() - Assertions.assertTrue( + assertTrue( Arc.container().requestContext().isActive, "Request context should be active", ) @@ -119,19 +178,19 @@ class RequestContextCoroutineContextTest { runTest { withContext(Dispatchers.IO.withCdiContext()) { // THEN the request context should be active - Assertions.assertTrue( + assertTrue( Arc.container().requestContext().isActive, "Request context should be active", ) // AND the number value should match the given number - Assertions.assertEquals(givenNumber, requestData.numberValue) + assertEquals(givenNumber, requestData.numberValue) assertThatCallersClassLoaderIsExpected() // AND the number value should match the given number after a short delay delay(10) - Assertions.assertEquals(givenNumber, requestData.numberValue) + assertEquals(givenNumber, requestData.numberValue) assertThatCallersClassLoaderIsExpected() @@ -141,7 +200,7 @@ class RequestContextCoroutineContextTest { // THEN the number value should match the expected post-async number after a short // delay delay(10) - Assertions.assertEquals(expectedPostAsyncNumber, requestData.numberValue) + assertEquals(expectedPostAsyncNumber, requestData.numberValue) assertThatCallersClassLoaderIsExpected() } @@ -149,14 +208,14 @@ class RequestContextCoroutineContextTest { // THEN the number value should match the expected post-async number after the block // execution - Assertions.assertEquals(expectedPostAsyncNumber, requestData.numberValue) + assertEquals(expectedPostAsyncNumber, requestData.numberValue) } @Test + @ActivateRequestContext fun `with an active request scope on async`() { // GIVEN an active request context - Arc.container().requestContext().activate() - Assertions.assertTrue( + assertTrue( Arc.container().requestContext().isActive, "Request context should be active", ) @@ -172,47 +231,47 @@ class RequestContextCoroutineContextTest { runTest { coroutineScope { async(Dispatchers.IO.withCdiContext()) { - // THEN the request context should be active - Assertions.assertTrue( - Arc.container().requestContext().isActive, - "Request context should be active", - ) + // THEN the request context should be active + assertTrue( + Arc.container().requestContext().isActive, + "Request context should be active", + ) - // AND the number value should match the given number - Assertions.assertEquals(givenNumber, requestData.numberValue) + // AND the number value should match the given number + assertEquals(givenNumber, requestData.numberValue) - assertThatCallersClassLoaderIsExpected() + assertThatCallersClassLoaderIsExpected() - // AND the number value should match the given number after a short delay - delay(10) - Assertions.assertEquals(givenNumber, requestData.numberValue) + // AND the number value should match the given number after a short delay + delay(10) + assertEquals(givenNumber, requestData.numberValue) - assertThatCallersClassLoaderIsExpected() + assertThatCallersClassLoaderIsExpected() - // WHEN we set the number value to the expected post-async number - requestData.numberValue = expectedPostAsyncNumber + // WHEN we set the number value to the expected post-async number + requestData.numberValue = expectedPostAsyncNumber - // THEN the number value should match the expected post-async number after a - // short delay - delay(10) - Assertions.assertEquals(expectedPostAsyncNumber, requestData.numberValue) + // THEN the number value should match the expected post-async number after a + // short delay + delay(10) + assertEquals(expectedPostAsyncNumber, requestData.numberValue) - assertThatCallersClassLoaderIsExpected() - } + assertThatCallersClassLoaderIsExpected() + } .await() } } // THEN the number value should match the expected post-async number after the block // execution - Assertions.assertEquals(expectedPostAsyncNumber, requestData.numberValue) + assertEquals(expectedPostAsyncNumber, requestData.numberValue) } @Test + @ActivateRequestContext fun `with an active request scope on inner coroutine scope in async`() { // GIVEN an active request context - Arc.container().requestContext().activate() - Assertions.assertTrue( + assertTrue( Arc.container().requestContext().isActive, "Request context should be active", ) @@ -228,53 +287,53 @@ class RequestContextCoroutineContextTest { runTest { coroutineScope { async(Dispatchers.IO.withCdiContext()) { - coroutineScope { - // THEN the request context should be active - Assertions.assertTrue( - Arc.container().requestContext().isActive, - "Request context should be active", - ) - - // AND the number value should match the given number - Assertions.assertEquals(givenNumber, requestData.numberValue) - - assertThatCallersClassLoaderIsExpected() - - // AND the number value should match the given number after a short - // delay - delay(10) - Assertions.assertEquals(givenNumber, requestData.numberValue) - - assertThatCallersClassLoaderIsExpected() - - // WHEN we set the number value to the expected post-async number - requestData.numberValue = expectedPostAsyncNumber - - // THEN the number value should match the expected post-async number - // after a short delay - delay(10) - Assertions.assertEquals( - expectedPostAsyncNumber, - requestData.numberValue, - ) - - assertThatCallersClassLoaderIsExpected() - } + coroutineScope { + // THEN the request context should be active + assertTrue( + Arc.container().requestContext().isActive, + "Request context should be active", + ) + + // AND the number value should match the given number + assertEquals(givenNumber, requestData.numberValue) + + assertThatCallersClassLoaderIsExpected() + + // AND the number value should match the given number after a short + // delay + delay(10) + assertEquals(givenNumber, requestData.numberValue) + + assertThatCallersClassLoaderIsExpected() + + // WHEN we set the number value to the expected post-async number + requestData.numberValue = expectedPostAsyncNumber + + // THEN the number value should match the expected post-async number + // after a short delay + delay(10) + assertEquals( + expectedPostAsyncNumber, + requestData.numberValue, + ) + + assertThatCallersClassLoaderIsExpected() } + } .await() } } // THEN the number value should match the expected post-async number after the block // execution - Assertions.assertEquals(expectedPostAsyncNumber, requestData.numberValue) + assertEquals(expectedPostAsyncNumber, requestData.numberValue) } @Test + @ActivateRequestContext fun `with a terminated request scope while on async (undefined behavior)`() { // GIVEN an active request context - Arc.container().requestContext().activate() - Assertions.assertTrue( + assertTrue( Arc.container().requestContext().isActive, "Request context should be active", ) @@ -292,41 +351,41 @@ class RequestContextCoroutineContextTest { runTest { val job = launch { async(Dispatchers.IO.withCdiContext()) { - // THEN the request context should be active - Assertions.assertTrue( - Arc.container().requestContext().isActive, - "Request context should be active", - ) + // THEN the request context should be active + assertTrue( + Arc.container().requestContext().isActive, + "Request context should be active", + ) - // AND the number value should match the given number - Assertions.assertEquals(givenNumber, requestData.numberValue) + // AND the number value should match the given number + assertEquals(givenNumber, requestData.numberValue) - assertThatCallersClassLoaderIsExpected() + assertThatCallersClassLoaderIsExpected() - asyncStarted.complete(Unit) + asyncStarted.complete(Unit) - // WHEN we wait for the request context to be terminated by another thread - requestScopeTerminated.await() + // WHEN we wait for the request context to be terminated by another thread + requestScopeTerminated.await() - // THEN the request context should not be active (undefined behavior) - Assertions.assertFalse( - Arc.container().requestContext().isActive, - "Request context should not be active", - ) - } + // THEN the request context should not be active (undefined behavior) + assertFalse( + Arc.container().requestContext().isActive, + "Request context should not be active", + ) + } .await() } launch { asyncStarted.await() - Assertions.assertTrue( + assertTrue( Arc.container().requestContext().isActive, "Request context should be active", ) - Assertions.assertEquals(givenNumber, requestData.numberValue) + assertEquals(givenNumber, requestData.numberValue) Arc.container().requestContext().terminate() - Assertions.assertFalse( + assertFalse( Arc.container().requestContext().isActive, "Request context should be active", ) @@ -354,13 +413,13 @@ class RequestContextCoroutineContextTest { val secondRequestState = Arc.container().requestContext().stateIfActive assertNotNull(secondRequestState) - Assertions.assertNotEquals(firstGivenNumber, requestData.numberValue) + assertNotEquals(firstGivenNumber, requestData.numberValue) val secondGivenNumber = 5432L val expectedSecondGivenNumber = 95432L requestData.numberValue = secondGivenNumber - Assertions.assertNotEquals(firstRequestState, secondRequestState) + assertNotEquals(firstRequestState, secondRequestState) val waitForFirstEnd = CompletableDeferred() val waitForSecondMiddle = CompletableDeferred() @@ -371,70 +430,70 @@ class RequestContextCoroutineContextTest { val jobFirstRequest = launch { Arc.container().requestContext().activate(firstRequestState) async(Dispatchers.IO.withCdiContext()) { - // THEN the request context should be active - Assertions.assertTrue( - Arc.container().requestContext().isActive, - "Request context should be active", - ) + // THEN the request context should be active + assertTrue( + Arc.container().requestContext().isActive, + "Request context should be active", + ) - // AND the number value should match the given number - Assertions.assertEquals(firstGivenNumber, requestData.numberValue) + // AND the number value should match the given number + assertEquals(firstGivenNumber, requestData.numberValue) - assertThatCallersClassLoaderIsExpected() + assertThatCallersClassLoaderIsExpected() - waitForSecondMiddle.await() + waitForSecondMiddle.await() - // THEN the request context should be active - Assertions.assertTrue( - Arc.container().requestContext().isActive, - "Request context should be active", - ) + // THEN the request context should be active + assertTrue( + Arc.container().requestContext().isActive, + "Request context should be active", + ) - // AND the number value should match the given number - Assertions.assertEquals(firstGivenNumber, requestData.numberValue) + // AND the number value should match the given number + assertEquals(firstGivenNumber, requestData.numberValue) - assertThatCallersClassLoaderIsExpected() + assertThatCallersClassLoaderIsExpected() - requestData.numberValue = expectedFirstGivenNumber + requestData.numberValue = expectedFirstGivenNumber - waitForFirstEnd.complete(Unit) - } + waitForFirstEnd.complete(Unit) + } .await() } val jobSecondRequest = launch { Arc.container().requestContext().activate(secondRequestState) async(Dispatchers.IO.withCdiContext()) { - // THEN the request context should be active - Assertions.assertTrue( - Arc.container().requestContext().isActive, - "Request context should be active", - ) + // THEN the request context should be active + assertTrue( + Arc.container().requestContext().isActive, + "Request context should be active", + ) - // AND the number value should match the given number - Assertions.assertEquals(secondGivenNumber, requestData.numberValue) + // AND the number value should match the given number + assertEquals(secondGivenNumber, requestData.numberValue) - assertThatCallersClassLoaderIsExpected() + assertThatCallersClassLoaderIsExpected() - waitForSecondMiddle.complete(Unit) + waitForSecondMiddle.complete(Unit) - waitForFirstEnd.await() + waitForFirstEnd.await() - // THEN the request context should be active - Assertions.assertTrue( - Arc.container().requestContext().isActive, - "Request context should be active", - ) + // THEN the request context should be active + assertTrue( + Arc.container().requestContext().isActive, + "Request context should be active", + ) - // AND the number value should match the given number - Assertions.assertEquals(secondGivenNumber, requestData.numberValue) + // AND the number value should match the given number + assertEquals(secondGivenNumber, requestData.numberValue) - assertThatCallersClassLoaderIsExpected() + assertThatCallersClassLoaderIsExpected() - requestData.numberValue = expectedSecondGivenNumber + requestData.numberValue = expectedSecondGivenNumber - waitForSecondEnd.complete(Unit) - } + waitForSecondEnd.complete(Unit) + } .await() } @@ -443,11 +502,11 @@ class RequestContextCoroutineContextTest { } Arc.container().requestContext().activate(secondRequestState) - Assertions.assertEquals(expectedSecondGivenNumber, requestData.numberValue) + assertEquals(expectedSecondGivenNumber, requestData.numberValue) Arc.container().requestContext().terminate() Arc.container().requestContext().activate(firstRequestState) - Assertions.assertEquals(expectedFirstGivenNumber, requestData.numberValue) + assertEquals(expectedFirstGivenNumber, requestData.numberValue) Arc.container().requestContext().terminate() }