All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.nike.riposte.util.AsyncNettyHelper Maven / Gradle / Ivy

There is a newer version: 0.20.0
Show newest version
package com.nike.riposte.util;

import com.nike.fastbreak.CircuitBreaker;
import com.nike.internal.util.Pair;
import com.nike.riposte.server.channelpipeline.ChannelAttributes;
import com.nike.riposte.server.http.HttpProcessingState;
import com.nike.riposte.server.http.ProxyRouterProcessingState;
import com.nike.riposte.server.http.RequestInfo;
import com.nike.riposte.util.asynchelperwrapper.BiConsumerWithTracingAndMdcSupport;
import com.nike.riposte.util.asynchelperwrapper.BiFunctionWithTracingAndMdcSupport;
import com.nike.riposte.util.asynchelperwrapper.CallableWithTracingAndMdcSupport;
import com.nike.riposte.util.asynchelperwrapper.ConsumerWithTracingAndMdcSupport;
import com.nike.riposte.util.asynchelperwrapper.FunctionWithTracingAndMdcSupport;
import com.nike.riposte.util.asynchelperwrapper.RunnableWithTracingAndMdcSupport;
import com.nike.riposte.util.asynchelperwrapper.SupplierWithTracingAndMdcSupport;
import com.nike.wingtips.Span;
import com.nike.wingtips.Tracer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

import java.util.Deque;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

import io.netty.channel.ChannelHandlerContext;

/**
 * Helper class that provides static methods for dealing with async stuff in Netty/Riposte, mainly providing easy ways
 * to support distributed tracing and logger MDC when hopping threads using {@link
 * java.util.concurrent.CompletableFuture}s. There are various {@code *WithTracingAndMdc(...)} methods for wrapping
 * objects with the same type that knows how to handle tracing and MDC. You can also call the various {@code
 * linkTracingAndMdcToCurrentThread(...)} and {@code unlinkTracingAndMdcFromCurrentThread(...)} methods directly if
 * you want to do it manually yourself.
 * 

* If you want to use the link and unlink methods manually to wrap some chunk of code, the general procedure looks like * this: *

 *  Pair<Deque<Span>, Map<String, String>> originalThreadInfo = null;
 *  try {
 *      originalThreadInfo = AsyncNettyHelper.linkTracingAndMdcToCurrentThread(...);
 *      // Code that needs wrapping goes here
 *  }
 *  finally {
 *      AsyncNettyHelper.unlinkTracingAndMdcFromCurrentThread(originalThreadInfo);
 *  }
 * 
* Following this procedure guarantees that the code you want wrapped will be wrapped successfully with whatever tracing * and MDC info you want, but when it finishes the trace and MDC info will be put back the way it was as if your code * never ran. If you deviate from this then you risk having traces stomp on each other or other weird interactions that * you never expect or predict can mess up your tracing, so make sure you know what you're doing and test it thoroughly * under load if you deviate from this procedure. * * @author Nic Munroe */ @SuppressWarnings("WeakerAccess") public class AsyncNettyHelper { private static final Logger logger = LoggerFactory.getLogger(AsyncNettyHelper.class); public static final Void VOID = null; // Intentionally protected - use the static methods. protected AsyncNettyHelper() { /* do nothing */ } /** * @return A {@link Runnable} that wraps the given original so that the given {@link ChannelHandlerContext}'s * distributed tracing and MDC information is registered with the thread and therefore available during execution * and unregistered after execution. */ public static Runnable runnableWithTracingAndMdc(Runnable runnable, ChannelHandlerContext ctx) { return new RunnableWithTracingAndMdcSupport(runnable, ctx); } /** * @return A {@link Runnable} that wraps the given original so that the given distributed tracing and MDC * information is registered with the thread and therefore available during execution and unregistered after * execution. */ public static Runnable runnableWithTracingAndMdc(Runnable runnable, Pair, Map> threadInfoToLink) { return new RunnableWithTracingAndMdcSupport(runnable, threadInfoToLink); } /** * @return A {@link Runnable} that wraps the given original so that the given distributed tracing and MDC * information is registered with the thread and therefore available during execution and unregistered after * execution. */ public static Runnable runnableWithTracingAndMdc(Runnable runnable, Deque distributedTraceStackToLink, Map mdcContextMapToLink) { return new RunnableWithTracingAndMdcSupport(runnable, distributedTraceStackToLink, mdcContextMapToLink); } /** * @return A {@link Callable} that wraps the given original so that the given {@link ChannelHandlerContext}'s * distributed tracing and MDC information is registered with the thread and therefore available during execution * and unregistered after execution. */ public static Callable callableWithTracingAndMdc(Callable callable, ChannelHandlerContext ctx) { return new CallableWithTracingAndMdcSupport<>(callable, ctx); } /** * @return A {@link Callable} that wraps the given original so that the given distributed tracing and MDC * information is registered with the thread and therefore available during execution and unregistered after * execution. */ public static Callable callableWithTracingAndMdc(Callable callable, Pair, Map> threadInfoToLink) { return new CallableWithTracingAndMdcSupport<>(callable, threadInfoToLink); } /** * @return A {@link Callable} that wraps the given original so that the given distributed tracing and MDC * information is registered with the thread and therefore available during execution and unregistered after * execution. */ public static Callable callableWithTracingAndMdc(Callable callable, Deque distributedTraceStackToLink, Map mdcContextMapToLink) { return new CallableWithTracingAndMdcSupport<>(callable, distributedTraceStackToLink, mdcContextMapToLink); } /** * @return A {@link Supplier} that wraps the given original so that the given {@link ChannelHandlerContext}'s * distributed tracing and MDC information is registered with the thread and therefore available during execution * and unregistered after execution. */ public static Supplier supplierWithTracingAndMdc(Supplier supplier, ChannelHandlerContext ctx) { return new SupplierWithTracingAndMdcSupport<>(supplier, ctx); } /** * @return A {@link Supplier} that wraps the given distributed tracing and MDC information is registered with the * thread and therefore available during execution and unregistered after execution. */ public static Supplier supplierWithTracingAndMdc(Supplier supplier, Pair, Map> threadInfoToLink) { return new SupplierWithTracingAndMdcSupport<>(supplier, threadInfoToLink); } /** * @return A {@link Supplier} that wraps the given distributed tracing and MDC information is registered with the * thread and therefore available during execution and unregistered after execution. */ public static Supplier supplierWithTracingAndMdc(Supplier supplier, Deque distributedTraceStackToLink, Map mdcContextMapToLink) { return new SupplierWithTracingAndMdcSupport<>(supplier, distributedTraceStackToLink, mdcContextMapToLink); } /** * @return A {@link Function} that wraps the given original so that the given {@link ChannelHandlerContext}'s * distributed tracing and MDC information is registered with the thread and therefore available during execution * and unregistered after execution. */ public static Function functionWithTracingAndMdc(Function fn, ChannelHandlerContext ctx) { return new FunctionWithTracingAndMdcSupport<>(fn, ctx); } /** * @return A {@link Function} that wraps the given original so that the given distributed tracing and MDC * information is registered with the thread and therefore available during execution and unregistered after * execution. */ public static Function functionWithTracingAndMdc( Function fn, Pair, Map> threadInfoToLink ) { return new FunctionWithTracingAndMdcSupport<>(fn, threadInfoToLink); } /** * @return A {@link Function} that wraps the given original so that the given distributed tracing and MDC * information is registered with the thread and therefore available during execution and unregistered after * execution. */ public static Function functionWithTracingAndMdc(Function fn, Deque distributedTraceStackToLink, Map mdcContextMapToLink) { return new FunctionWithTracingAndMdcSupport<>(fn, distributedTraceStackToLink, mdcContextMapToLink); } /** * @return A {@link BiFunction} that wraps the given original so that the given {@link ChannelHandlerContext}'s * distributed tracing and MDC information is registered with the thread and therefore available during execution * and unregistered after execution. */ public static BiFunction biFunctionWithTracingAndMdc(BiFunction fn, ChannelHandlerContext ctx) { return new BiFunctionWithTracingAndMdcSupport<>(fn, ctx); } /** * @return A {@link BiFunction} that wraps the given original so that the given distributed tracing and MDC * information is registered with the thread and therefore available during execution and unregistered after * execution. */ public static BiFunction biFunctionWithTracingAndMdc( BiFunction fn, Pair, Map> threadInfoToLink ) { return new BiFunctionWithTracingAndMdcSupport<>(fn, threadInfoToLink); } /** * @return A {@link BiFunction} that wraps the given original so that the given distributed tracing and MDC * information is registered with the thread and therefore available during execution and unregistered after * execution. */ public static BiFunction biFunctionWithTracingAndMdc(BiFunction fn, Deque distributedTraceStackToLink, Map mdcContextMapToLink) { return new BiFunctionWithTracingAndMdcSupport<>(fn, distributedTraceStackToLink, mdcContextMapToLink); } /** * @return A {@link Consumer} that wraps the given original so that the given {@link ChannelHandlerContext}'s * distributed tracing and MDC information is registered with the thread and therefore available during execution * and unregistered after execution. */ public static Consumer consumerWithTracingAndMdc(Consumer consumer, ChannelHandlerContext ctx) { return new ConsumerWithTracingAndMdcSupport<>(consumer, ctx); } /** * @return A {@link Consumer} that wraps the given original so that the given distributed tracing and MDC * information is registered with the thread and therefore available during execution and unregistered after * execution. */ public static Consumer consumerWithTracingAndMdc(Consumer consumer, Pair, Map> threadInfoToLink) { return new ConsumerWithTracingAndMdcSupport<>(consumer, threadInfoToLink); } /** * @return A {@link Consumer} that wraps the given original so that the given distributed tracing and MDC * information is registered with the thread and therefore available during execution and unregistered after * execution. */ public static Consumer consumerWithTracingAndMdc(Consumer consumer, Deque distributedTraceStackToLink, Map mdcContextMapToLink) { return new ConsumerWithTracingAndMdcSupport<>(consumer, distributedTraceStackToLink, mdcContextMapToLink); } /** * @return A {@link BiConsumer} that wraps the given original so that the given {@link ChannelHandlerContext}'s * distributed tracing and MDC information is registered with the thread and therefore available during execution * and unregistered after execution. */ public static BiConsumer biConsumerWithTracingAndMdc(BiConsumer biConsumer, ChannelHandlerContext ctx) { return new BiConsumerWithTracingAndMdcSupport<>(biConsumer, ctx); } /** * @return A {@link BiConsumer} that wraps the given original so that the given distributed tracing and MDC * information is registered with the thread and therefore available during execution and unregistered after * execution. */ public static BiConsumer biConsumerWithTracingAndMdc( BiConsumer biConsumer, Pair, Map> threadInfoToLink ) { return new BiConsumerWithTracingAndMdcSupport<>(biConsumer, threadInfoToLink); } /** * @return A {@link BiConsumer} that wraps the given original so that the given distributed tracing and MDC * information is registered with the thread and therefore available during execution and unregistered after * execution. */ public static BiConsumer biConsumerWithTracingAndMdc(BiConsumer biConsumer, Deque distributedTraceStackToLink, Map mdcContextMapToLink) { return new BiConsumerWithTracingAndMdcSupport<>(biConsumer, distributedTraceStackToLink, mdcContextMapToLink); } /** * Extracts the {@link HttpProcessingState} from the given {@link ChannelHandlerContext}'s channel and uses the * resulting {@link HttpProcessingState#getDistributedTraceStack()} and {@link * HttpProcessingState#getLoggerMdcContextMap()} to setup distributed tracing and the logging MDC on the current * thread with the values contained in the state by calling {@link #linkTracingAndMdcToCurrentThread(Deque, Map)}. * * @param ctx * The {@link ChannelHandlerContext} to use to extract the {@link HttpProcessingState}, and from the state * extract the distributed tracing and MDC info to link to the thread. This method safely handles null - if null * is found for the ctx argument or the state inside then {@link Tracer} will be setup with an empty trace stack * (wiping out any existing in-progress traces) *and* {@link org.slf4j.MDC#clear()} will be called (wiping out * any existing MDC info). * * @return A *COPY* of the original trace stack and MDC info on the thread when this method was called (before being * replaced with the given arguments). The {@link Pair} object will never be null, but the values it contains may be * null. A copy is returned rather than the original to prevent undesired behavior (storing the return value and * then passing it in to {@link #unlinkTracingAndMdcFromCurrentThread(Pair)} later should *guarantee* that after * calling that unlink method the thread state is exactly as it was right *before* calling this link method. If we * returned the original trace stack this contract guarantee could be violated). */ public static Pair, Map> linkTracingAndMdcToCurrentThread(ChannelHandlerContext ctx) { if (ctx == null) return linkTracingAndMdcToCurrentThread(null, null); HttpProcessingState state = ChannelAttributes.getHttpProcessingStateForChannel(ctx).get(); if (state == null) return linkTracingAndMdcToCurrentThread(null, null); return linkTracingAndMdcToCurrentThread(state.getDistributedTraceStack(), state.getLoggerMdcContextMap()); } /** * Links the given distributed tracing and logging MDC info to the current thread. Any existing tracing and MDC info * on the current thread will be wiped out and overridden, so if you need to go back to them in the future you'll * need to store the copy info returned by this method for later. * * @param threadInfoToLink * A {@link Pair} containing the distributed trace stack and MDC info you want to link to the current thread. * This argument can be null - if it is null then {@link Tracer} will be setup with an empty trace stack (wiping * out any existing in-progress traces) *and* {@link org.slf4j.MDC#clear()} will be called (wiping out any * existing MDC info). The left and/or right portion of the pair can also be null, with any null portion of the * pair causing the corresponding portion to be emptied/cleared while letting any non-null portion link to the * thread as expected. * * @return A *COPY* of the original trace stack and MDC info on the thread when this method was called (before being * replaced with the given arguments). The {@link Pair} object will never be null, but the values it contains may be * null. A copy is returned rather than the original to prevent undesired behavior (storing the return value and * then passing it in to {@link #unlinkTracingAndMdcFromCurrentThread(Pair)} later should *guarantee* that after * calling that unlink method the thread state is exactly as it was right *before* calling this link method. If we * returned the original trace stack this contract guarantee could be violated). */ public static Pair, Map> linkTracingAndMdcToCurrentThread( Pair, Map> threadInfoToLink ) { Deque distributedTraceStack = (threadInfoToLink == null) ? null : threadInfoToLink.getLeft(); Map mdcContextMap = (threadInfoToLink == null) ? null : threadInfoToLink.getRight(); return linkTracingAndMdcToCurrentThread(distributedTraceStack, mdcContextMap); } /** * Links the given distributed tracing and logging MDC info to the current thread. Any existing tracing and MDC info * on the current thread will be wiped out and overridden, so if you need to go back to them in the future you'll * need to store the copy info returned by this method for later. * * @param distributedTraceStackToLink * The stack of distributed traces that should be associated with the current thread. This can be null - if it * is null then {@link Tracer} will be setup with an empty trace stack (wiping out any existing in-progress * traces). * @param mdcContextMapToLink * The MDC context map to associate with the current thread. This can be null - if it is null then {@link * org.slf4j.MDC#clear()} will be called (wiping out any existing MDC info). * * @return A *COPY* of the original trace stack and MDC info on the thread when this method was called (before being * replaced with the given arguments). The {@link Pair} object will never be null, but the values it contains may be * null. A copy is returned rather than the original to prevent undesired behavior (storing the return value and * then passing it in to {@link #unlinkTracingAndMdcFromCurrentThread(Pair)} later should *guarantee* that after * calling that unlink method the thread state is exactly as it was right *before* calling this link method. If we * returned the original trace stack this contract guarantee could be violated). */ public static Pair, Map> linkTracingAndMdcToCurrentThread( Deque distributedTraceStackToLink, Map mdcContextMapToLink ) { // Unregister the trace stack so that if there's already a trace on the stack we don't get exceptions when // registering the desired stack with the thread, and keep a copy of the results. Map callingThreadMdcContextMap = MDC.getCopyOfContextMap(); Deque callingThreadTraceStack = Tracer.getInstance().unregisterFromThread(); // Now setup the trace stack and MDC as desired if (mdcContextMapToLink == null) MDC.clear(); else MDC.setContextMap(mdcContextMapToLink); Tracer.getInstance().registerWithThread(distributedTraceStackToLink); // Return the copied original data so that it can be re-linked later (if the caller wants) return Pair.of(callingThreadTraceStack, callingThreadMdcContextMap); } /** * Helper method for calling {@link #unlinkTracingAndMdcFromCurrentThread(Deque, Map)} that * gracefully handles the case where the pair you pass in is null - if the pair you pass in is null then {@link * #unlinkTracingAndMdcFromCurrentThread(Deque, Map)} will be called with both arguments null. */ public static void unlinkTracingAndMdcFromCurrentThread( Pair, Map> threadInfoToResetFor) { Deque traceStackToResetFor = (threadInfoToResetFor == null) ? null : threadInfoToResetFor.getLeft(); Map mdcContextMapToResetFor = (threadInfoToResetFor == null) ? null : threadInfoToResetFor.getRight(); unlinkTracingAndMdcFromCurrentThread(traceStackToResetFor, mdcContextMapToResetFor); } /** * Calls {@link Tracer#unregisterFromThread()} and {@link org.slf4j.MDC#clear()} to reset this thread's tracing and * MDC state to be completely clean, then (optionally) resets the trace stack and MDC info to the arguments * provided. If the trace stack argument is null then the trace stack will *not* be reset, and similarly if the MDC * info is null then the MDC info will *not* be reset. So if both are null then when this method finishes the trace * stack and MDC will be left in a blank state. */ public static void unlinkTracingAndMdcFromCurrentThread(Deque distributedTraceStackToResetFor, Map mdcContextMapToResetFor) { Tracer.getInstance().unregisterFromThread(); MDC.clear(); if (mdcContextMapToResetFor != null) MDC.setContextMap(mdcContextMapToResetFor); if (distributedTraceStackToResetFor != null) Tracer.getInstance().registerWithThread(distributedTraceStackToResetFor); } /** * @param ctx * The {@link ChannelHandlerContext} that holds the current request state. * * @return Pulls the distributed tracing span stack and MDC information from the {@link HttpProcessingState} stored * in the given {@link ChannelHandlerContext}, or null in the case {@code ctx} is null or does not contain a {@link * HttpProcessingState}. */ public static Pair, Map> extractTracingAndMdcInfoFromChannelHandlerContext( ChannelHandlerContext ctx) { if (ctx == null) return null; HttpProcessingState state = ChannelAttributes.getHttpProcessingStateForChannel(ctx).get(); if (state == null) return null; return Pair.of(state.getDistributedTraceStack(), state.getLoggerMdcContextMap()); } /** * Executes the given runnable only if {@code ctx.channel().isActive()} returns true. If the channel is not active * then a warning is logged, resources are released, the distributed trace is completed (if appropriate), and the * runnable is *not* executed. Call this when you're about to do something and it's possible that the channel has * been closed. Usually this is only necessary when you're manually firing an event on the given {@code ctx} after * some asynchronous delay (e.g. a future completes, or a timeout was scheduled on the event loop, etc). * * @param ctx * The {@link ChannelHandlerContext} that contains the state for this request. * @param markerForLogs * This will be put into the log warning if the channel is not active to help you identify where the problem * occurred. This is usually some arbitrary "ID" representing the code that is calling this method. * @param thingToMaybeExecute * This will be executed if the channel is active, and ignored if the channel is not active. * * @return true if the channel was active and the thingToMaybeExecute was executed, false if the channel was not * active and things were cleaned up as per this method description. */ public static boolean executeOnlyIfChannelIsActive(ChannelHandlerContext ctx, String markerForLogs, Runnable thingToMaybeExecute) { if (ctx.channel().isActive()) { // The channel is active, so execute the runnable. thingToMaybeExecute.run(); return true; } else { Pair, Map> origTracingAndMdcPair = linkTracingAndMdcToCurrentThread(ctx); try { // The channel is *not* active. Log a warning, release resources, // and complete any existing distributed trace. logger.warn( "Unable to continue - channel is no longer active. The client may have closed the connection. " + "Releasing resources and stopping request processing. channel_inactive_cannot_continue_marker={}", markerForLogs ); // Gather the stuff we want to try to release resources for. HttpProcessingState state = ChannelAttributes.getHttpProcessingStateForChannel(ctx).get(); RequestInfo requestInfo = (state == null) ? null : state.getRequestInfo(); ProxyRouterProcessingState proxyRouterState = ChannelAttributes.getProxyRouterProcessingStateForChannel(ctx).get(); // Tell the RequestInfo it can release all its resources. if (requestInfo != null) requestInfo.releaseAllResources(); // Tell the ProxyRouterProcessingState that the stream failed and trigger its chunk streaming error // handling with an artificial exception. If the call had already succeeded previously then this // will do nothing, but if it hasn't already succeeded then it's not going to (since the connection // is closing) and doing this will cause any resources it's holding onto to be released. if (proxyRouterState != null) { Throwable reason = new RuntimeException("Cannot execute - Server worker channel closed"); proxyRouterState.cancelRequestStreaming(reason, ctx); proxyRouterState.cancelDownstreamRequest(reason); } // Complete the trace only if there's no state, or if we have a state but the trace hasn't been // completed yet. If the state says the trace has already been completed we don't want to spit it // out a second time. if (state == null || !state.isTraceCompletedOrScheduled()) { Tracer.getInstance().completeRequestSpan(); // If we have a state then indicate that the span has already been completed. if (state != null) state.setTraceCompletedOrScheduled(true); } return false; } finally { unlinkTracingAndMdcFromCurrentThread(origTracingAndMdcPair); } } } /** * Helper method for creating a `CompletableFuture` that is using the tracing helpers. *

*

     * AsyncNettyHelper.supplyAsync(() -> {
     *      //do some work in a background thread
     *      return VOID;
     * }, executor, ctx);
     * 
*/ public static CompletableFuture supplyAsync(Supplier f, Executor executor, ChannelHandlerContext ctx) { return CompletableFuture.supplyAsync(supplierWithTracingAndMdc(f, ctx), executor); } /** * Helper method for creating a `CompletableFuture` that is using the tracing helpers and has the CompletableFuture * wrapped in a `CircuitBreaker` *

*

     * AsyncNettyHelper.supplyAsync(() -> {
     *      //do some work in a background thread
     *      return VOID;
     * }, circuitBreaker, executor, ctx);
     * 
*/ public static CompletableFuture supplyAsync(Supplier f, CircuitBreaker circuitBreaker, Executor executor, ChannelHandlerContext ctx) { return circuitBreaker.executeAsyncCall(() -> supplyAsync(f, executor, ctx)); } /** * Helper method for creating a `CompletableFuture` that is wrapped by a CircuitBreaker and * has the `Supplier` wrapped around a SubSpan. *

* You would prefer this method over {@link #supplyAsync(Supplier, Executor, ChannelHandlerContext)} or * {@link #supplyAsync(Supplier, CircuitBreaker, Executor, ChannelHandlerContext)} * when your `Supplier` has logic that makes an outbound/downstream call. This will net you distributed tracing logs. *

* An example would be using a client SDK that makes blocking HTTP calls. *

* The SubSpan purpose will be set to `CLIENT` as this is the typical use case when utilizing these helpers. *

*

     * AsyncNettyHelper.supplyAsync("someWorkToBeDone", () -> {
     *      //do some work in a background thread
     *      return VOID;
     * }, circuitBreaker, executor, ctx);
     * 
*/ public static CompletableFuture supplyAsync(String subSpanName, Supplier f, CircuitBreaker circuitBreaker, Executor executor, ChannelHandlerContext ctx) { return circuitBreaker.executeAsyncCall(() -> supplyAsync(subSpanName, f, executor, ctx)); } /** * Helper method for creating a `CompletableFuture` that has the `Supplier` wrapped around a SubSpan. *

* You would prefer this method over the above when your `Supplier` has logic that makes an outbound/downstream call * and you do not want the use of a `CircuitBreaker`. *

* You would prefer this method over {@link #supplyAsync(String, Supplier, CircuitBreaker, Executor, ChannelHandlerContext)} * when your `Supplier` has logic that you would like wrapped with distributed tracing logs and not use a {@link CircuitBreaker} *

* An example would be using a client SDK that makes blocking HTTP calls. *

* The SubSpan purpose will be set to `CLIENT` as this is the typical use case when utilizing these helpers. *

*

     * AsyncNettyHelper.supplyAsync("someWorkToBeDone", () -> {
     *      //do some work in a background thread
     *      return VOID;
     * }, executor, ctx);
     * 
*/ public static CompletableFuture supplyAsync(String subSpanName, Supplier f, Executor executor, ChannelHandlerContext ctx) { return CompletableFuture.supplyAsync(supplierWithTracingAndMdc(() -> { try { Tracer.getInstance().startSubSpan(subSpanName, Span.SpanPurpose.CLIENT); return f.get(); } finally { Tracer.getInstance().completeSubSpan(); } }, ctx), executor); } }