I have a simple test class as following
@OptIn(ExperimentalCoroutinesApi::class)
@DataR2dbcTest
@Import(TransactionLogTest.TestService::class)
class TransactionLogTest(
@Autowired private val testService: TestService,
) {
@Test
fun `test log`() = runTest {
withContext(MDCContext(mapOf("key" to "value"))) {
assertThat(MDC.get("key")).isEqualTo("value")
testService.doSomething()
}
}
open class TestService {
@Transactional
open suspend fun doSomething() {
assertThat(MDC.get("key")).isEqualTo("value")
}
}
}
The assertion in the doSomething
function will fail which indicates the Kotlin coroutines context is not propagated correctly to the called suspend function.
I investigated further and I think I found the root cause. In org.springframework.transaction.interceptor.TransactionAspectSupport
the code snippet from invokeWithinTransaction
method as following uses CoroutinesUtils.invokeSuspendingFunction
to execute the wrapped method
InvocationCallback callback = invocation;
if (corInv != null) {
callback = () -> CoroutinesUtils.invokeSuspendingFunction(method, corInv.getTarget(), corInv.getArguments());
}
However CoroutinesUtils.invokeSuspendingFunction
doesn't care about the current continuation/context but create a new one by
MonoKt.mono
call as following
public static Publisher<?> invokeSuspendingFunction(Method method, Object target, Object... args) {
KFunction<?> function = Objects.requireNonNull(ReflectJvmMapping.getKotlinFunction(method));
KClassifier classifier = function.getReturnType().getClassifier();
Mono<Object> mono = MonoKt.mono(Dispatchers.getUnconfined(), (scope, continuation) ->
KCallables.callSuspend(function, getSuspendedFunctionArgs(target, args), continuation))
.filter(result -> !Objects.equals(result, Unit.INSTANCE))
.onErrorMap(InvocationTargetException.class, InvocationTargetException::getTargetException);
if (classifier != null && classifier.equals(JvmClassMappingKt.getKotlinClass(Flow.class))) {
return mono.flatMapMany(CoroutinesUtils::asFlux);
}
return mono;
}
I am not sure what should be a proper fix for this but potentially we can pass the actual continuation or extract it from the args
and use that instead of the new one from MonoKt.mono
Comment From: sdeleuze
Can't reproduce with Spring Boot 3.0.2
and Kotlin 1.7.22
.
Comment From: sdeleuze
Even if I was not able to reproduce with the provided sample, the underlying issue with Coroutines context and transactions is still present is is tracked by #27308.