In the olden days, we had ThreadLocal
for programs to carry data along with the request path since all request processing was done on that thread and stuff like Log
My solution theme would be to (It would work with JDK 9+ as a couple of overridable methods are exposed since that version)
Make the complete ecosystem aware of MDC
And for that, we need to address the following scenarios:
For that, let's create a MDC aware version class of CompletableFuture
by extending it. My version of that would look like below
import org.slf4j.MDC;
import java.util.Map;
import java.util.concurrent.*;
import java.util.function.Function;
import java.util.function.Supplier;
public class MDCAwareCompletableFuture extends CompletableFuture {
public static final ExecutorService MDC_AWARE_ASYNC_POOL = new MDCAwareForkJoinPool();
@Override
public CompletableFuture newIncompleteFuture() {
return new MDCAwareCompletableFuture();
}
@Override
public Executor defaultExecutor() {
return MDC_AWARE_ASYNC_POOL;
}
public static CompletionStage getMDCAwareCompletionStage(CompletableFuture future) {
return new MDCAwareCompletableFuture<>()
.completeAsync(() -> null)
.thenCombineAsync(future, (aVoid, value) -> value);
}
public static CompletionStage getMDCHandledCompletionStage(CompletableFuture future,
Function throwableFunction) {
Map contextMap = MDC.getCopyOfContextMap();
return getMDCAwareCompletionStage(future)
.handle((value, throwable) -> {
setMDCContext(contextMap);
if (throwable != null) {
return throwableFunction.apply(throwable);
}
return value;
});
}
}
The MDCAwareForkJoinPool
class would look like (have skipped the methods with ForkJoinTask
parameters for simplicity)
public class MDCAwareForkJoinPool extends ForkJoinPool {
//Override constructors which you need
@Override
public ForkJoinTask submit(Callable task) {
return super.submit(MDCUtility.wrapWithMdcContext(task));
}
@Override
public ForkJoinTask submit(Runnable task, T result) {
return super.submit(wrapWithMdcContext(task), result);
}
@Override
public ForkJoinTask> submit(Runnable task) {
return super.submit(wrapWithMdcContext(task));
}
@Override
public void execute(Runnable task) {
super.execute(wrapWithMdcContext(task));
}
}
The utility methods to wrap would be such as
public static Callable wrapWithMdcContext(Callable task) {
//save the current MDC context
Map contextMap = MDC.getCopyOfContextMap();
return () -> {
setMDCContext(contextMap);
try {
return task.call();
} finally {
// once the task is complete, clear MDC
MDC.clear();
}
};
}
public static Runnable wrapWithMdcContext(Runnable task) {
//save the current MDC context
Map contextMap = MDC.getCopyOfContextMap();
return () -> {
setMDCContext(contextMap);
try {
return task.run();
} finally {
// once the task is complete, clear MDC
MDC.clear();
}
};
}
public static void setMDCContext(Map contextMap) {
MDC.clear();
if (contextMap != null) {
MDC.setContextMap(contextMap);
}
}
Below are some guidelines for usage:
MDCAwareCompletableFuture
rather than the class CompletableFuture
.CompletableFuture
instantiates the self version such as new CompletableFuture...
. For such methods (most of the public static methods), use an alternative method to get an instance of MDCAwareCompletableFuture
. An example of using an alternative could be rather than using CompletableFuture.supplyAsync(...)
, you can choose new MDCAwareCompletableFuture<>().completeAsync(...)
CompletableFuture
to MDCAwareCompletableFuture
by using the method getMDCAwareCompletionStage
when you get stuck with one because of say some external library which returns you an instance of CompletableFuture
. Obviously, you can't retain the context within that library but this method would still retain the context after your code hits the application code.MDCAwareForkJoinPool
. You could create MDCAwareThreadPoolExecutor
by overriding execute
method as well to serve your use case. You get the idea!You can find a detailed explanation of all of the above here in a post about the same.