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.