Copying MDC through Custom Thread Pool Executor

The Unknown
2 min readJun 12, 2021

--

MDC (Mapped Diagnostic Context) is used to set contextual information about the HTTP request like request id, user id etc., and can be made available on all log statements through Pattern Layout. This will liberate developers from handling contextual information at every log statement.

MDC in Multi-Threading

As this contextual information is maintained at a thread level, when a thread switch happens the new thread will not have this contextual information. The contextual information need to be copied to the new thread to avoid missing information like request id, user id etc., in log statements.

Copying the Context Information

Executor Service is on of the java tools to create a thread pool and execute tasks in the thread pool. Without a custom executor, copying MDC context would like below

ExecutorService executorService = Executors.newFixedThreadPool(10);
final Map<String, String> contextMap = MDC.getCopyOfContextMap();
executorService.execute(new Runnable() {
public void run() {
System.out.println("Copying context");
MDC.setContextMap(contextMap);
// Do Task related stuff
}
});
executorService.execute(new Runnable() {
public void run() {
System.out.println("Copying context");
MDC.setContextMap(contextMap);
// Do Task related stuff
}
});

Concerns

  • The developers need to be aware of copying the context every time a thread switch happens.
  • The same copy statement is repeated at multiple places deviating from the common software principles like DRY

Custom Thread Pool Executor

Using custom thread pool executor we can offload the MDC copying responsibility to a single class which deals with this. Below is an example implementation of custom thread pool executor.

public class CustomThreadPoolExecutor extends ThreadPoolExecutor {
public CustomThreadPoolExecutor(
final int corePoolSize,
final int maximumPoolSize,
final long keepAliveTime,
final TimeUnit unit,
final BlockingQueue<Runnable> workQueue
) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
public void execute(final Runnable command) {
super.execute(wrapWithContext(command));
}

private Runnable wrapWithContext(final Runnable task) {
Map<String, String> contextMap = MDC.getCopyOfContextMap();
return () -> {
MDC.setContextMap(contextMap);
task.run();
};
}
}

Below is a wrapper factory class to create instances of CustomThreadPoolExecutor

public final class CustomExecutors {
private CustomExecutors() {
}
public static ExecutorService newFixedThreadPool(final int nThreads) {
return new CustomThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
}

Using custom executor to execute tasks

ExecutorService executorService = CustomExecutors.newFixedThreadPool(10);
executorService.execute(new Runnable() {
public void run() {
// Do Task related stuff
}
});
executorService.execute(new Runnable() {
public void run() {
// Do Task related stuff
}
});

--

--

No responses yet