Search code examples
spring-bootkotlinspring-webfluxkotlin-coroutines

MDCContext got empty after i do run awaitSingle of a Mono (Spring webflux with kotlin suspend coroutine )


I want to ask as why MDCContext got empty after calling awaitSingle of a Mono

here is snippet of the code

suspend fun getTemplates(request: GetPaginatedTemplateServiceRequest): PaginationTemplates {
        var context = MDCContext()
        val count = templateRepository.findByName(
            request.templateName
        ).awaitSingle()
        context = MDCContext()
}

for your information templaterepository is using ReactiveMongo.

the first call of MDCContext, I have some x-request-id in the context map which i generated before that function

but after i call that awaitSingle with Mono, the MDCContext got empty (which I am not expected)

One solution that I try is to wrap awaitSingle with withContext function

example below

        var context = MDCContext()
        val count = withContext(Dispatchers.IO.plus(MDCContext())) {
            templateRepository.findTemplatesCount(
                request.templateName
            ).awaitSingle()
        }
        context = MDCContext()

then the second call of mdc context still containing items like the first call (which i expected).

I actually can use that solution, but there is so many already code that use awaitSingle like that.is there any simple solution for that? might be like automatically passing context to mono or something?

thankss, let me know if it is not clear enough


Solution

  • The MDCContext needs to be explicitly managed across the asynchronous boundaries. Unfortunately, ReactiveMongo does not inherently handle the propagation of this context.

    There is not an automatic way to pass the context to every Mono operation, unless the reactive library explicitly supports this context propagation. Some reactive frameworks might automatically propagate contexts, while others might not.

    In essence, the current solution of using withContext is the most practical method available to maintain the context across awaitSingle() calls.

    Continue using the withContext method you've already implemented to ensure context propagation; and consider encapsulating this into reusable utilities wrapping the asynchronous operations that require the context.

    The following is a draft example of encapsulating the context propagation logic.

    import kotlinx.coroutines.Dispatchers
    import kotlinx.coroutines.withContext
    import org.slf4j.MDC
    import reactor.core.publisher.Mono
    
    suspend fun <T> withMdcContext(block: suspend () -> T): T {
        val contextMap = MDC.getCopyOfContextMap() ?: emptyMap()
        return withContext(Dispatchers.IO + MDCContext(contextMap)) {
            block()
        }
    }
    
    // Extension function for context preservation
    fun <T> Mono<T>.preserveContext(): Mono<T> =
        this.subscriberContext { context ->
            val mdcContext = context.get(MDCContext::class.java)
            context + (mdcContext ?: MDCContext())
        }
    

    Calling preserveContext() explicitly ensures that any context set before the reactive operation remains throughout the whole reactive chain. It's a proactive step to safeguard context integrity in scenarios where the default behavior of context propagation might not be consistent across various reactive libraries.

    suspend fun getTemplates(request: GetPaginatedTemplateServiceRequest): PaginationTemplates {
        val count = withMdcContext {
            templateRepository.findByName(request.templateName)
                .preserveContext() // Ensure context preservation in Mono
                .awaitSingle()
        }
        // ...
    }