Dans notre logiciel, nous utilisons largement MDC pour suivre des éléments tels que les identifiants de session et les noms d'utilisateur pour les requêtes Web. Cela fonctionne bien lors de l'exécution dans le thread d'origine. Cependant, beaucoup de choses doivent être traitées en arrière-plan. Pour cela, nous utilisons les classes Java.concurrent.ThreadPoolExecutor
et Java.util.Timer
avec certains services d'exécution asynchrones auto-lancés. Tous ces services gèrent leur propre pool de threads.
C'est ce que Le manuel de Logback a à dire sur l'utilisation de MDC dans un tel environnement:
Une copie du contexte de diagnostic mappé ne peut pas toujours être héritée par les threads de travail à partir du thread initiateur. C'est le cas lorsque Java.util.concurrent.Executors est utilisé pour la gestion des threads. Par exemple, la méthode newCachedThreadPool crée un ThreadPoolExecutor et, comme tout autre code de pool de threads, elle possède une logique de création de thread complexe.
Dans ce cas, il est recommandé d'appeler MDC.getCopyOfContextMap () sur le thread (maître) d'origine avant de soumettre une tâche à l'exécuteur. Lorsque la tâche est exécutée, sa première action doit appeler MDC.setContextMapValues () pour associer la copie stockée des valeurs MDC d'origine au nouveau thread géré par l'exécuteur.
Ce serait très bien, mais il est très facile d’oublier d’ajouter ces appels et il n’est pas facile de reconnaître le problème avant qu’il ne soit trop tard. Le seul signe avec Log4j est que vous perdez des informations MDC dans les journaux et avec Logback, vous obtenez des informations MDC obsolètes (car le thread du pool de foulées hérite de son MDC de la première tâche exécutée). Les deux sont des problèmes graves dans un système de production.
Je ne vois pas notre situation spéciale en aucune façon, pourtant je n'ai pas trouvé grand chose à propos de ce problème sur le web. Apparemment, beaucoup de gens ne se heurtent pas à cette situation et il doit donc y avoir un moyen de l'éviter. Qu'est-ce que nous faisons mal ici?
Oui, c'est un problème commun que j'ai rencontré aussi. Il existe quelques solutions de contournement (telles que le réglage manuel, comme décrit), mais vous souhaitez idéalement une solution qui
Callable
avec MyCallable
partout ou une laideur similaire).Voici une solution que j'utilise qui répond à ces trois besoins. Le code devrait être explicite.
(Remarque: cet exécuteur peut être créé et transmis à la MoreExecutors.listeningDecorator()
de Guava, sivous utilisez la ListanableFuture
de Guava.)
import org.slf4j.MDC;
import Java.util.Map;
import Java.util.concurrent.*;
/**
* A SLF4J MDC-compatible {@link ThreadPoolExecutor}.
* <p/>
* In general, MDC is used to store diagnostic information (e.g. a user's session id) in per-thread variables, to facilitate
* logging. However, although MDC data is passed to thread children, this doesn't work when threads are reused in a
* thread pool. This is a drop-in replacement for {@link ThreadPoolExecutor} sets MDC data before each task appropriately.
* <p/>
* Created by jlevy.
* Date: 6/14/13
*/
public class MdcThreadPoolExecutor extends ThreadPoolExecutor {
final private boolean useFixedContext;
final private Map<String, Object> fixedContext;
/**
* Pool where task threads take MDC from the submitting thread.
*/
public static MdcThreadPoolExecutor newWithInheritedMdc(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue) {
return new MdcThreadPoolExecutor(null, corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
/**
* Pool where task threads take fixed MDC from the thread that creates the pool.
*/
@SuppressWarnings("unchecked")
public static MdcThreadPoolExecutor newWithCurrentMdc(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue) {
return new MdcThreadPoolExecutor(MDC.getCopyOfContextMap(), corePoolSize, maximumPoolSize, keepAliveTime, unit,
workQueue);
}
/**
* Pool where task threads always have a specified, fixed MDC.
*/
public static MdcThreadPoolExecutor newWithFixedMdc(Map<String, Object> fixedContext, int corePoolSize,
int maximumPoolSize, long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
return new MdcThreadPoolExecutor(fixedContext, corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
private MdcThreadPoolExecutor(Map<String, Object> fixedContext, int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
this.fixedContext = fixedContext;
useFixedContext = (fixedContext != null);
}
@SuppressWarnings("unchecked")
private Map<String, Object> getContextForTask() {
return useFixedContext ? fixedContext : MDC.getCopyOfContextMap();
}
/**
* All executions will have MDC injected. {@code ThreadPoolExecutor}'s submission methods ({@code submit()} etc.)
* all delegate to this.
*/
@Override
public void execute(Runnable command) {
super.execute(wrap(command, getContextForTask()));
}
public static Runnable wrap(final Runnable runnable, final Map<String, Object> context) {
return new Runnable() {
@Override
public void run() {
Map previous = MDC.getCopyOfContextMap();
if (context == null) {
MDC.clear();
} else {
MDC.setContextMap(context);
}
try {
runnable.run();
} finally {
if (previous == null) {
MDC.clear();
} else {
MDC.setContextMap(previous);
}
}
}
};
}
}
Nous avons rencontré un problème similaire. Vous voudrez peut-être étendre ThreadPoolExecutor et remplacer les méthodes before/afterExecute pour passer les appels MDC dont vous avez besoin avant de démarrer/arrêter de nouveaux threads.
IMHO la meilleure solution est de:
ThreadPoolTaskExecutor
TaskDecorator
executor.setTaskDecorator(new LoggingTaskDecorator());
Le décorateur peut ressembler à ceci:
private final class LoggingTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable task) {
// web thread
Map<String, String> webThreadContext = MDC.getCopyOfContextMap();
return () -> {
// work thread
try {
// TODO: is this thread safe?
MDC.setContextMap(webThreadContext);
task.run();
} finally {
MDC.clear();
}
};
}
}
Comme pour les solutions précédemment publiées, les méthodes newTaskFor
pour Runnable
et Callable
peuvent être remplacées afin de boucler l'argument (voir la solution acceptée) lors de la création de RunnableFuture
.
Remarque: Par conséquent, la méthode executorService
's submit
doit être appelée à la place de la méthode execute
.
Pour la ScheduledThreadPoolExecutor
, les méthodes decorateTask
seraient écrasées à la place.
J'ai pu résoudre cela en utilisant l'approche suivante
Dans le fil principal (Application.Java, le point d'entrée de mon application)
static public Map<String, String> mdcContextMap = MDC.getCopyOfContextMap();
Dans la méthode d'exécution de la classe appelée par Executer
MDC.setContextMap(Application.mdcContextMap);
Voici comment je le fais avec les pools de threads et les exécuteurs fixes:
ExecutorService executor = Executors.newFixedThreadPool(4);
Map<String, String> mdcContextMap = MDC.getCopyOfContextMap();
Dans la partie filetée:
executor.submit(() -> {
MDC.setContextMap(mdcContextMap);
// my stuff
});