Как отлаживать тупики CompletableStage? - PullRequest
0 голосов
/ 02 мая 2018

Самая сложная проблема отладки, с которой я столкнулся в последнее время, это взаимоблокировки между асинхронными операциями. Например, для двух CompletionStage цепочек, где первая цепочка вызывает метод, который зависит от завершения второй цепочки, а вторая цепочка вызывает метод, который зависит от завершения первой цепочки. Это не так очевидно в реальной жизни, потому что зависимость имеет тенденцию быть скрытой, и иногда в тупиках участвуют более трех сторон.

Часть проблемы в том, что нет способа узнать, что ожидает CompletableStage. Это потому, что операция ссылается на CompletableStage, а не наоборот.

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

1 Ответ

0 голосов
/ 15 мая 2018

Я закончил тем, что сделал следующее:

  • В конце каждой цепочки CompletionStage запланируйте событие, которое сработает по истечении времени ожидания:

    Set<Object> knownDeadlocks = ConcurrentHashMap.newKeySet();
    // ...
    Future<?> deadlockListener = scope.getScheduler().schedule(() ->
    {
        if (knownDeadlocks.add(Throwables.getStackTraceAsString(context)))
            log.warn("Possible deadlock", context);
    }, DEADLOCK_DURATION.toMillis(), TimeUnit.MILLISECONDS);
    
  • Используйте CompletionStage.handle() для отключения deadlockListener, если этап завершается, как ожидалось:

    return stage.handle((value, throwable) ->
    {
        // WARNING: By design, CompletionStage.whenComplete() suppresses any exceptions thrown by its argument, so we use handle() instead.
        deadlockListener.cancel(false);
        if (throwable == null)
            return value;
        return rethrowException(throwable);
    });
    
  • Для полноты, у вас также есть:

    /**
     * Rethrows a {@code Throwable}, wrapping it in {@code CompletionException} if it isn't already wrapped.
     *
     * @param <T>       the return type expected by the caller
     * @param throwable a Throwable
     * @return an undefined value (the method always throws an exception)
     * @throws CompletionException wraps {@code throwable}
     */
    public <T> T rethrowException(Throwable throwable)
    {
        if (throwable instanceof CompletionException)
            throw (CompletionException) throwable;
        if (throwable == null)
            throwable = new NullPointerException("throwable may not be null");
        // According to https://stackoverflow.com/a/49261367/14731 some methods do not wrap exceptions
        throw new CompletionException(throwable);
    }
    
...