Manual context propagation in Quarkus native mode

3.7k views Asked by At

I'm trying to get context propagation working in Quarkus native mode.
The code below works as expected in the JVM mode, but returns MDC value: null in the native mode.
By "as expected" I mean:
Response to curl http://localhost:8080/thread-context is MDC value: from-thread-context

@Inject
ManagedExecutor managedExecutor;

@Inject
ThreadContext threadContext;

private final Supplier<String> mdcValueSupplier =
        () -> "MDC value:  " + MDC.get("foo") + "\n";

@GET
@Path("thread-context")
public String get() throws ExecutionException, InterruptedException {
    MDC.put("foo", "from-thread-context");
    Supplier<String> ctxSupplier = threadContext.contextualSupplier(mdcValueSupplier);
    return managedExecutor.supplyAsync(ctxSupplier).get();
}

I've created a github repo with full code of demo app and step-by-step instruction to reproduce the issue.

Dependency io.quarkus:quarkus-smallrye-context-propagation is present.
Quarkus version: 1.9.2

Q: Is it the issue with my code, or with Quarkus?

For the reference: Quarkus documentatin on context propagation

1

There are 1 answers

2
Ladicek On BEST ANSWER

Your code is essentially fine [1], and Quarkus is also fine for that matter -- but there are two things to understand.

One, you're not doing any kind of "manual context propagation". Your code works by accident, because Quarkus uses JBoss LogManager as the logger, and its MDC isn't an ordinary ThreadLocal, it's an InheritableThreadLocal. So it kinda sorta sometimes propagates the context itself. But that's nothing to rely on. If you for example do a live reload (by modifying the code a bit and running curl again), it will stop working in JVM mode too.

Two, the very point of context propagation is to transfer thread-local state from one thread to another, but that doesn't happen automagically. Either you do that yourself (that would be "manual context propagation"), by calling the respective APIs, or you can implement a ThreadContextProvider.

I took a brief look at the MDC API (http://www.slf4j.org/api/org/slf4j/MDC.html) and it seems rudimentary context propagation can be implemented with getCopyOfContextMap and setContextMap. Here's an implementation I put together quickly -- beware, I didn't test the code too much:

import org.eclipse.microprofile.context.spi.ThreadContextProvider;
import org.eclipse.microprofile.context.spi.ThreadContextSnapshot;
import org.slf4j.MDC;

import java.util.Map;

public class MdcContextProvider implements ThreadContextProvider {
    @Override
    public ThreadContextSnapshot currentContext(Map<String, String> props) {
        Map<String, String> propagate = MDC.getCopyOfContextMap();
        return () -> {
            Map<String, String> old = MDC.getCopyOfContextMap();
            MDC.setContextMap(propagate);
            return () -> {
                MDC.setContextMap(old);
            };
        };
    }

    @Override
    public ThreadContextSnapshot clearedContext(Map<String, String> props) {
        return () -> {
            Map<String, String> old = MDC.getCopyOfContextMap();
            MDC.clear();
            return () -> {
                MDC.setContextMap(old);
            };
        };
    }

    @Override
    public String getThreadContextType() {
        return "SLF4J MDC";
    }
}

If you create a META-INF/services/org.eclipse.microprofile.context.spi.ThreadContextProvider file containing the fully qualified name of this class, then MDC propagation should work for you, even in native.

One possible issue with this is that whatever changes you do to MDC on the new thread will not be propagated back to the original thread, because SLF4J intentionally doesn't provide access to the backing map, it only hands out copies. That might be OK for you, or not.

[1] You don't have to "contextualize" your Supplier by ThreadContext.contextualSupplier if you submit it to ManagedExecutor -- the ManagedExecutor does that automatically.