web-dev-qa-db-fra.com

Comment utiliser MDC avec des pools de threads?

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?

113
Lóránt Pintér

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

  • Définit le MDC de manière cohérente;
  • Évite les bogues tacites pour lesquels le MDC est incorrect mais que vous ne connaissez pas. et
  • Minimise les modifications apportées à la manière dont vous utilisez les pools de threads (par exemple, sous-classer 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);
                    }
                }
            }
        };
    }
}
68
jlevy

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.

25
Mark

IMHO la meilleure solution est de:

  • utiliser ThreadPoolTaskExecutor
  • mettre en œuvre votre propre TaskDecorator
  • utilisez-le: 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();
            }
        };
    }

}
9
Tomáš Myšík

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.

2
MyKey_

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);
0
smishra

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
});
0
Amaury D