Как использовать MDC с пулами потоков?

В нашем программном обеспечении мы широко используем MDC для отслеживания таких вещей, как идентификаторы сеансов и имена пользователей для веб-запросов. Это отлично работает при работе в исходном потоке. Тем не менее, есть много вещей, которые нужно обрабатывать в фоновом режиме. Для этого мы используем классы java.concurrent.ThreadPoolExecutor и java.util.Timer, а также некоторые самораскрывающиеся службы выполнения async. Все эти службы управляют собственным пулом потоков.

Это то, что Руководство по протоколу должно говорить об использовании MDC в такой среде:

Копия отображаемого диагностического контекста не всегда может быть унаследована рабочими потоками из инициирующего потока. Это тот случай, когда java.util.concurrent.Executors используется для управления потоками. Например, метод newCachedThreadPool создает ThreadPoolExecutor и, как и другой код объединения потоков, имеет сложную логику создания потоков.

В таких случаях рекомендуется, чтобы MDC.getCopyOfContextMap() вызывается в исходном (основном) потоке перед отправкой задачи исполнителю. Когда задача запускается в качестве ее первого действия, она должна вызывать MDC.setContextMapValues ​​(), чтобы связать сохраненную копию исходных значений MDC с новым управляемым потоком Executor.

Это было бы хорошо, но очень легко забыть добавлять эти вызовы, и нет простого способа распознать проблему, пока не станет слишком поздно. Единственный признак с Log4j заключается в том, что вы получаете информацию о MDC в журналах, а с помощью Logback вы получаете устаревшую информацию MDC (поскольку поток в пуле протектора наследует свой MDC от первой задачи, которая была запущена на нем). Оба являются серьезными проблемами в производственной системе.

Я не вижу особую ситуацию в нашей ситуации, но я не мог найти много об этой проблеме в Интернете. По-видимому, это не то, что многие люди сталкиваются, поэтому должен быть способ избежать этого. Что мы делаем неправильно здесь?

Ответ 1

Да, это обычная проблема, с которой я столкнулся. Существует несколько способов обхода (например, вручную, как описано), но в идеале вам нужно решение, которое

  • Устанавливает MDC последовательно;
  • Предотвращает неявные ошибки, когда MDC неверен, но вы этого не знаете; и
  • Сведение к минимуму изменений в том, как вы используете пулы потоков (например, подклассы Callable с MyCallable везде или подобное уродство).

Здесь решение, которое я использую, отвечает этим трем потребностям. Код должен быть понятным.

(В качестве примечания, этот исполнитель может быть создан и отправлен в Guava MoreExecutors.listeningDecorator(), если вы используете Guava ListanableFuture.)

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 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} 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);
                    }
                }
            }
        };
    }
}

Ответ 2

Мы столкнулись с аналогичной проблемой. Возможно, вы захотите расширить ThreadPoolExecutor и переопределить методы berfore/afterExecute, чтобы выполнить вызовы MDC, которые вам нужны, прежде чем запускать/останавливать новые потоки.

Ответ 3

Как и ранее опубликованные решения, методы newTaskFor для Runnable и Callable могут быть перезаписаны для того чтобы обернуть аргумент (см. принятое решение) при создании RunnableFuture.

Примечание. Следовательно, метод executorService submit должен вызываться вместо метода execute.

Для ScheduledThreadPoolExecutor вместо этого будут перезаписаны методы decorateTask.

Ответ 4

ИМХО лучшее решение:

  • реализовать свой собственный TaskDecorator
  • используйте его: executor.setTaskDecorator(new LoggingTaskDecorator());

Декоратор может выглядеть так:

private final class LoggingTaskDecorator implements TaskDecorator {

    @Override
    public Runnable decorate(Runnable task) {
        // web thread
        Map<String, String> webThreadContext = MDC.getCopyOfContextMap();
        return () -> {
            // work thread
            try {
                MDC.setContextMap(webThreadContext);
                task.run();
            } finally {
                MDC.clear();
            }
        };
    }

}

Ответ 5

Я смог решить это, используя следующий подход

В основном потоке (Application.java, моя точка входа в приложение)

static public Map<String, String> mdcContextMap = MDC.getCopyOfContextMap();

В методе run класса, вызываемого Executer

MDC.setContextMap(Application.mdcContextMap);

Ответ 6

Вот как я это делаю с фиксированными пулами потоков и исполнителями:

ExecutorService executor = Executors.newFixedThreadPool(4);
Map<String, String> mdcContextMap = MDC.getCopyOfContextMap();

В части с резьбой:

executor.submit(() -> {
    MDC.setContextMap(mdcContextMap);
    // my stuff
});