Does CompletableFuture have a corresponding Local context?

后端 未结 2 2110
执念已碎
执念已碎 2021-02-06 06:26

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

相关标签:
2条回答
  • 2021-02-06 07:01

    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:

    • When all do we get new instances of CompletableFuture from within this class? → We need to return a MDC aware version of the same rather.
    • When all do we get new instances of CompletableFuture from outside this class? → We need to return a MDC aware version of the same rather.
    • Which executor is used when in CompletableFuture class? → In all circumstances, we need to make sure that all executors are MDC aware

    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<T> extends CompletableFuture<T> {
    
        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 <T> CompletionStage<T> getMDCAwareCompletionStage(CompletableFuture<T> future) {
            return new MDCAwareCompletableFuture<>()
                    .completeAsync(() -> null)
                    .thenCombineAsync(future, (aVoid, value) -> value);
        }
    
        public static <T> CompletionStage<T> getMDCHandledCompletionStage(CompletableFuture<T> future,
                                                                    Function<Throwable, T> throwableFunction) {
            Map<String, String> 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 <T> ForkJoinTask<T> submit(Callable<T> task) {
            return super.submit(MDCUtility.wrapWithMdcContext(task));
        }
    
        @Override
        public <T> ForkJoinTask<T> 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 <T> Callable<T> wrapWithMdcContext(Callable<T> task) {
        //save the current MDC context
        Map<String, String> 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<String, String> 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<String, String> contextMap) {
       MDC.clear();
       if (contextMap != null) {
           MDC.setContextMap(contextMap);
        }
    }
    

    Below are some guidelines for usage:

    • Use the class MDCAwareCompletableFuture rather than the class CompletableFuture.
    • A couple of methods in the class 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(...)
    • Convert the instance of 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.
    • While supplying an executor as a parameter, make sure that it is MDC Aware such as 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.

    0 讨论(0)
  • 2021-02-06 07:05

    If you come across this, just poke the thread here http://mail.openjdk.java.net/pipermail/core-libs-dev/2017-May/047867.html

    to implement something like twitter Futures which transfer Locals (Much like ThreadLocal but transfers state).

    See the def respond() method in here and how it calls Locals.save() and Locals.restort() https://github.com/simonratner/twitter-util/blob/master/util-core/src/main/scala/com/twitter/util/Future.scala

    If Java Authors would fix this, then the MDC in logback would work across all 3rd party libraries. Until then, IT WILL NOT WORK unless you can change the 3rd party library(doubtful you can do that).

    0 讨论(0)
提交回复
热议问题