From eec151b86b6b1d567f49717a71010e78a759d183 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Thu, 28 Apr 2022 06:20:05 +0200 Subject: [PATCH] fix: correct Kotlin coroutine interceptions (#7247) --- .../intercepted/KotlinInterceptedMethod.java | 18 ++----- .../suspend/multiple/CustomRepository.kt | 29 +++++++++++ .../suspend/multiple/InterceptorSpec.kt | 48 +++++++++++++++++ .../server/suspend/multiple/MyRepository.kt | 25 +++++++++ .../multiple/MyRepositoryInterceptorImpl.kt | 46 ++++++++++++++++ .../docs/server/suspend/multiple/MyService.kt | 35 +++++++++++++ .../server/suspend/multiple/Transaction1.kt | 30 +++++++++++ .../multiple/Transaction1Interceptor.kt | 52 +++++++++++++++++++ .../server/suspend/multiple/Transaction2.kt | 30 +++++++++++ .../multiple/Transaction2Interceptor.kt | 51 ++++++++++++++++++ 10 files changed, 350 insertions(+), 14 deletions(-) create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/CustomRepository.kt create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/InterceptorSpec.kt create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyRepository.kt create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyRepositoryInterceptorImpl.kt create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyService.kt create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction1.kt create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction1Interceptor.kt create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction2.kt create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction2Interceptor.kt diff --git a/aop/src/main/java/io/micronaut/aop/internal/intercepted/KotlinInterceptedMethod.java b/aop/src/main/java/io/micronaut/aop/internal/intercepted/KotlinInterceptedMethod.java index b71cbff6ed9..cba013d544a 100644 --- a/aop/src/main/java/io/micronaut/aop/internal/intercepted/KotlinInterceptedMethod.java +++ b/aop/src/main/java/io/micronaut/aop/internal/intercepted/KotlinInterceptedMethod.java @@ -100,13 +100,8 @@ public Argument returnTypeValue() { @Override public CompletableFuture interceptResultAsCompletionStage() { - CompletableFutureContinuation completableFutureContinuation; - if (continuation instanceof CompletableFutureContinuation) { - completableFutureContinuation = (CompletableFutureContinuation) continuation; - } else { - completableFutureContinuation = new CompletableFutureContinuation(continuation); - replaceContinuation.accept(completableFutureContinuation); - } + CompletableFutureContinuation completableFutureContinuation = new CompletableFutureContinuation(continuation); + replaceContinuation.accept(completableFutureContinuation); Object result = context.proceed(); replaceContinuation.accept(continuation); if (result != KotlinUtils.COROUTINE_SUSPENDED) { @@ -117,13 +112,8 @@ public CompletableFuture interceptResultAsCompletionStage() { @Override public CompletableFuture interceptResultAsCompletionStage(Interceptor from) { - CompletableFutureContinuation completableFutureContinuation; - if (continuation instanceof CompletableFutureContinuation) { - completableFutureContinuation = (CompletableFutureContinuation) continuation; - } else { - completableFutureContinuation = new CompletableFutureContinuation(continuation); - replaceContinuation.accept(completableFutureContinuation); - } + CompletableFutureContinuation completableFutureContinuation = new CompletableFutureContinuation(continuation); + replaceContinuation.accept(completableFutureContinuation); Object result = context.proceed(from); replaceContinuation.accept(continuation); if (result != KotlinUtils.COROUTINE_SUSPENDED) { diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/CustomRepository.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/CustomRepository.kt new file mode 100644 index 00000000000..31c9a61bb10 --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/CustomRepository.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend.multiple + +@MyRepository +interface CustomRepository { + + suspend fun xyz(): String + + suspend fun abc(): String + + suspend fun count1(): String + + suspend fun count2(): String + +} \ No newline at end of file diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/InterceptorSpec.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/InterceptorSpec.kt new file mode 100644 index 00000000000..ba0881b1fbd --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/InterceptorSpec.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend.multiple + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.ints.shouldBeExactly +import io.kotest.matchers.shouldBe +import io.micronaut.context.ApplicationContext +import kotlinx.coroutines.runBlocking + +class InterceptorSpec : StringSpec() { + + val context = autoClose( + ApplicationContext.run() + ) + + private var myService = context.getBean(MyService::class.java) + + init { + "test correct interceptors calls" { + runBlocking { + myService.someCall() + MyService.events.size shouldBeExactly 8 + MyService.events[0] shouldBe "intercept1-start" + MyService.events[1] shouldBe "intercept2-start" + MyService.events[2] shouldBe "repository-abc" + MyService.events[3] shouldBe "repository-xyz" + MyService.events[4] shouldBe "intercept2-end" + MyService.events[5] shouldBe "intercept1-end" + MyService.events[6] shouldBe "repository-count1" + MyService.events[7] shouldBe "repository-count2" + } + } + } +} diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyRepository.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyRepository.kt new file mode 100644 index 00000000000..55ed35fee4d --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyRepository.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend.multiple + +import io.micronaut.aop.Introduction +import jakarta.inject.Singleton + +@MustBeDocumented +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +@Introduction +@Singleton +annotation class MyRepository() diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyRepositoryInterceptorImpl.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyRepositoryInterceptorImpl.kt new file mode 100644 index 00000000000..0590cfeddad --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyRepositoryInterceptorImpl.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend.multiple + +import io.micronaut.aop.InterceptedMethod +import io.micronaut.aop.InterceptorBean +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import jakarta.inject.Singleton +import java.io.IOException +import java.util.concurrent.CompletableFuture + +@InterceptorBean(MyRepository::class) +@Singleton +class MyRepositoryInterceptorImpl : MethodInterceptor { + override fun intercept(context: MethodInvocationContext?): Any? { + val interceptedMethod = InterceptedMethod.of(context) + return try { + if (interceptedMethod.resultType() == InterceptedMethod.ResultType.COMPLETION_STAGE) { + MyService.events.add("repository-" + context!!.methodName) + val cf: CompletableFuture = CompletableFuture.supplyAsync{ + Thread.sleep(1000) + context!!.methodName + } + interceptedMethod.handleResult(cf) + } else { + throw IllegalStateException() + } + } catch (e: Exception) { + interceptedMethod.handleException(e) + } + } +} \ No newline at end of file diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyService.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyService.kt new file mode 100644 index 00000000000..7da2e746538 --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyService.kt @@ -0,0 +1,35 @@ +package io.micronaut.docs.server.suspend.multiple + +import jakarta.inject.Singleton +import java.util.* +import kotlin.collections.ArrayList + +@Singleton +open class MyService( + private val repository: CustomRepository +) { + + companion object { + val events: MutableList = Collections.synchronizedList(ArrayList()) + } + + open suspend fun someCall() { + // Simulate accessing two different data-source repositories using two transactions + tx1() + // Call another coroutine + repository.count1() + repository.count2() + } + + @Transaction1 + open suspend fun tx1() { + tx2() + } + + @Transaction2 + open suspend fun tx2() { + repository.abc() + repository.xyz() + } + +} diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction1.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction1.kt new file mode 100644 index 00000000000..60722c0556e --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction1.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend.multiple + +import io.micronaut.aop.Around +import kotlin.annotation.AnnotationRetention.RUNTIME +import kotlin.annotation.AnnotationTarget.CLASS +import kotlin.annotation.AnnotationTarget.FILE +import kotlin.annotation.AnnotationTarget.FUNCTION +import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER +import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER + +@MustBeDocumented +@Retention(RUNTIME) +@Target(CLASS, FILE, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER) +@Around +annotation class Transaction1 diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction1Interceptor.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction1Interceptor.kt new file mode 100644 index 00000000000..d48d3afea70 --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction1Interceptor.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend.multiple + +import io.micronaut.aop.InterceptedMethod +import io.micronaut.aop.InterceptorBean +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import jakarta.inject.Singleton +import java.util.concurrent.CompletableFuture +import java.util.function.BiConsumer + +@InterceptorBean(Transaction1::class) +@Singleton +class Transaction1Interceptor : MethodInterceptor { + override fun intercept(context: MethodInvocationContext): Any? { + val interceptedMethod = InterceptedMethod.of(context) + return try { + return if (interceptedMethod.resultType() == InterceptedMethod.ResultType.COMPLETION_STAGE) { + MyService.events.add("intercept1-start") + val completionStage = interceptedMethod.interceptResultAsCompletionStage() + val cf = CompletableFuture() + completionStage.whenComplete { value, throwable -> + MyService.events.add("intercept1-end") + if (throwable == null) { + cf.complete(value) + } else { + cf.completeExceptionally(throwable) + } + } + interceptedMethod.handleResult(cf) + } else { + throw IllegalStateException() + } + } catch (e: Exception) { + interceptedMethod.handleException(e) + } + } +} \ No newline at end of file diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction2.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction2.kt new file mode 100644 index 00000000000..d32723cf1cf --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction2.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend.multiple + +import io.micronaut.aop.Around +import kotlin.annotation.AnnotationRetention.RUNTIME +import kotlin.annotation.AnnotationTarget.CLASS +import kotlin.annotation.AnnotationTarget.FILE +import kotlin.annotation.AnnotationTarget.FUNCTION +import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER +import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER + +@MustBeDocumented +@Retention(RUNTIME) +@Target(CLASS, FILE, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER) +@Around +annotation class Transaction2 diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction2Interceptor.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction2Interceptor.kt new file mode 100644 index 00000000000..7fe950117e7 --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction2Interceptor.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend.multiple + +import io.micronaut.aop.InterceptedMethod +import io.micronaut.aop.InterceptorBean +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import jakarta.inject.Singleton +import java.util.concurrent.CompletableFuture + +@InterceptorBean(Transaction2::class) +@Singleton +class Transaction2Interceptor : MethodInterceptor { + override fun intercept(context: MethodInvocationContext): Any? { + val interceptedMethod = InterceptedMethod.of(context) + return try { + return if (interceptedMethod.resultType() == InterceptedMethod.ResultType.COMPLETION_STAGE) { + MyService.events.add("intercept2-start") + val completionStage = interceptedMethod.interceptResultAsCompletionStage() + val cf = CompletableFuture() + completionStage.whenComplete { value, throwable -> + MyService.events.add("intercept2-end") + if (throwable == null) { + cf.complete(value) + } else { + cf.completeExceptionally(throwable) + } + } + interceptedMethod.handleResult(cf) + } else { + throw IllegalStateException() + } + } catch (e: Exception) { + interceptedMethod.handleException(e) + } + } +} \ No newline at end of file