如何调试 CompletableStage 死锁?

How to debug CompletableStage deadlocks?

我最近 运行 遇到的最困难的调试问题是异步操作之间的死锁。例如,给定两个 CompletionStage 链,其中第一个链调用一个方法,该方法依赖于第二个链的完成,第二个链调用一个方法,该方法依赖于第一个链的完成。这在现实生活中并不那么明显,因为依赖往往是隐藏的,有时死锁涉及三个以上的参与方。

部分问题是无法找出 CompletableStage 正在等待什么。这是因为操作引用了 CompletableStage,而不是相反。

现在大多数调试器都提供某种程度的死锁检测,但这只适用于线程。 如何调试CompletableStage链之间的死锁?

我最后做了以下事情:

  • 在每个 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  some methods do not wrap exceptions
        throw new CompletionException(throwable);
    }