How to create async stacktraces?

后端 未结 2 2099
余生分开走
余生分开走 2020-12-30 06:12

UPDATE: The latest version of Intellij IDEA implements exactly what I\'m looking for. The question is how to implement this outside of the IDE (so I can to

相关标签:
2条回答
  • 2020-12-30 06:45

    Seeing as this question has not received any answers in almost a month, I'm going to post the best solution I've found to date:

    DebugCompletableFuture.java:

    /**
     * A {@link CompletableFuture} that eases debugging.
     *
     * @param <T> the type of value returned by the future
     */
    public final class DebugCompletableFuture<T> extends CompletableFuture<T>
    {
        private static RunMode RUN_MODE = RunMode.DEBUG;
        private static final Set<String> CLASS_PREFIXES_TO_REMOVE = ImmutableSet.of(DebugCompletableFuture.class.getName(),
            CompletableFuture.class.getName(), ThreadPoolExecutor.class.getName());
        private static final Set<Class<? extends Throwable>> EXCEPTIONS_TO_UNWRAP = ImmutableSet.of(AsynchronousException.class,
            CompletionException.class, ExecutionException.class);
        private final CompletableFuture<T> delegate;
        private final AsynchronousException asyncStacktrace;
    
        /**
         * @param delegate the stage to delegate to
         * @throws NullPointerException if any of the arguments are null
         */
        private DebugCompletableFuture(CompletableFuture<T> delegate)
        {
            requireThat("delegate", delegate).isNotNull();
            this.delegate = delegate;
            this.asyncStacktrace = new AsynchronousException();
            delegate.whenComplete((value, exception) ->
            {
                if (exception == null)
                {
                    super.complete(value);
                    return;
                }
                exception = Exceptions.unwrap(exception, EXCEPTIONS_TO_UNWRAP);
                asyncStacktrace.initCause(exception);
                filterStacktrace(asyncStacktrace, element ->
                {
                    String className = element.getClassName();
                    for (String prefix : CLASS_PREFIXES_TO_REMOVE)
                        if (className.startsWith(prefix))
                            return true;
                    return false;
                });
                Set<String> newMethods = getMethodsInStacktrace(asyncStacktrace);
                if (!newMethods.isEmpty())
                {
                    Set<String> oldMethods = getMethodsInStacktrace(exception);
                    newMethods.removeAll(oldMethods);
                    if (!newMethods.isEmpty())
                    {
                        // The async stacktrace introduces something new
                        super.completeExceptionally(asyncStacktrace);
                        return;
                    }
                }
                super.completeExceptionally(exception);
            });
        }
    
        /**
         * @param exception an exception
         * @return the methods referenced by the stacktrace
         * @throws NullPointerException if {@code exception} is null
         */
        private Set<String> getMethodsInStacktrace(Throwable exception)
        {
            requireThat("exception", exception).isNotNull();
            Set<String> result = new HashSet<>();
            for (StackTraceElement element : exception.getStackTrace())
                result.add(element.getClassName() + "." + element.getMethodName());
            for (Throwable suppressed : exception.getSuppressed())
                result.addAll(getMethodsInStacktrace(suppressed));
            return result;
        }
    
        /**
         * @param <T2>     the type returned by the delegate
         * @param delegate the stage to delegate to
         * @return if {@code RUN_MODE == DEBUG} returns an instance that wraps {@code delegate}; otherwise, returns {@code delegate}
         * unchanged
         * @throws NullPointerException if any of the arguments are null
         */
        public static <T2> CompletableFuture<T2> wrap(CompletableFuture<T2> delegate)
        {
            if (RUN_MODE != RunMode.DEBUG)
                return delegate;
            return new DebugCompletableFuture<>(delegate);
        }
    
        /**
         * Removes stack trace elements that match a filter. The exception and its descendants are processed recursively.
         * <p>
         * This method can be used to remove lines that hold little value for the end user (such as the implementation of utility functions).
         *
         * @param exception     the exception to process
         * @param elementFilter returns true if the current stack trace element should be removed
         */
        private void filterStacktrace(Throwable exception, Predicate<StackTraceElement> elementFilter)
        {
            Throwable cause = exception.getCause();
            if (cause != null)
                filterStacktrace(cause, elementFilter);
            for (Throwable suppressed : exception.getSuppressed())
                filterStacktrace(suppressed, elementFilter);
            StackTraceElement[] elements = exception.getStackTrace();
            List<StackTraceElement> keep = new ArrayList<>(elements.length);
            for (StackTraceElement element : elements)
            {
                if (!elementFilter.test(element))
                    keep.add(element);
            }
            exception.setStackTrace(keep.toArray(new StackTraceElement[0]));
        }
    
        @Override
        public <U> CompletableFuture<U> thenApply(Function<? super T, ? extends U> fn)
        {
            return wrap(super.thenApply(fn));
        }
    
        @Override
        public <U> CompletableFuture<U> thenApplyAsync(Function<? super T, ? extends U> fn)
        {
            return wrap(super.thenApplyAsync(fn));
        }
    
        @Override
        public <U> CompletableFuture<U> thenApplyAsync(Function<? super T, ? extends U> fn, Executor executor)
        {
            return wrap(super.thenApplyAsync(fn, executor));
        }
    
        @Override
        public CompletableFuture<Void> thenAccept(Consumer<? super T> action)
        {
            return wrap(super.thenAccept(action));
        }
    
        @Override
        public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action)
        {
            return wrap(super.thenAcceptAsync(action));
        }
    
        @Override
        public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action, Executor executor)
        {
            return wrap(super.thenAcceptAsync(action, executor));
        }
    
        @Override
        public CompletableFuture<Void> thenRun(Runnable action)
        {
            return wrap(super.thenRun(action));
        }
    
        @Override
        public CompletableFuture<Void> thenRunAsync(Runnable action)
        {
            return wrap(super.thenRunAsync(action));
        }
    
        @Override
        public CompletableFuture<Void> thenRunAsync(Runnable action, Executor executor)
        {
            return wrap(super.thenRunAsync(action, executor));
        }
    
        @Override
        public <U, V> CompletableFuture<V> thenCombine(CompletionStage<? extends U> other,
                                                       BiFunction<? super T, ? super U, ? extends V> fn)
        {
            return wrap(super.thenCombine(other, fn));
        }
    
        @Override
        public <U, V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other,
                                                            BiFunction<? super T, ? super U, ? extends V> fn)
        {
            return wrap(super.thenCombineAsync(other, fn));
        }
    
        @Override
        public <U, V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other,
                                                            BiFunction<? super T, ? super U, ? extends V> fn,
                                                            Executor executor)
        {
            return wrap(super.thenCombineAsync(other, fn, executor));
        }
    
        @Override
        public <U> CompletableFuture<Void> thenAcceptBoth(CompletionStage<? extends U> other,
                                                          BiConsumer<? super T, ? super U> action)
        {
            return wrap(super.thenAcceptBoth(other, action));
        }
    
        @Override
        public <U> CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,
                                                               BiConsumer<? super T, ? super U> action)
        {
            return wrap(super.thenAcceptBothAsync(other, action));
        }
    
        @Override
        public <U> CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,
                                                               BiConsumer<? super T, ? super U> action,
                                                               Executor executor)
        {
            return wrap(super.thenAcceptBothAsync(other, action, executor));
        }
    
        @Override
        public CompletableFuture<Void> runAfterBoth(CompletionStage<?> other, Runnable action)
        {
            return wrap(super.runAfterBoth(other, action));
        }
    
        @Override
        public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other, Runnable action)
        {
            return wrap(super.runAfterBothAsync(other, action));
        }
    
        @Override
        public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other, Runnable action, Executor executor)
        {
            return wrap(super.runAfterBothAsync(other, action, executor));
        }
    
        @Override
        public <U> CompletableFuture<U> applyToEither(CompletionStage<? extends T> other, Function<? super T, U> fn)
        {
            return wrap(super.applyToEither(other, fn));
        }
    
        @Override
        public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T, U> fn)
        {
            return wrap(super.applyToEitherAsync(other, fn));
        }
    
        @Override
        public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T, U> fn,
                                                           Executor executor)
        {
            return wrap(super.applyToEitherAsync(other, fn, executor));
        }
    
        @Override
        public CompletableFuture<Void> acceptEither(CompletionStage<? extends T> other, Consumer<? super T> action)
        {
            return wrap(super.acceptEither(other, action));
        }
    
        @Override
        public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action)
        {
            return wrap(super.acceptEitherAsync(other, action));
        }
    
        @Override
        public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action,
                                                         Executor executor)
        {
            return wrap(super.acceptEitherAsync(other, action, executor));
        }
    
        @Override
        public CompletableFuture<Void> runAfterEither(CompletionStage<?> other, Runnable action)
        {
            return wrap(super.runAfterEither(other, action));
        }
    
        @Override
        public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other, Runnable action)
        {
            return wrap(super.runAfterEitherAsync(other, action));
        }
    
        @Override
        public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other, Runnable action, Executor executor)
        {
            return wrap(super.runAfterEitherAsync(other, action, executor));
        }
    
        @Override
        public <U> CompletableFuture<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn)
        {
            return wrap(super.thenCompose(fn));
        }
    
        @Override
        public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn)
        {
            return wrap(super.thenComposeAsync(fn));
        }
    
        @Override
        public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn,
                                                         Executor executor)
        {
            return wrap(super.thenComposeAsync(fn, executor));
        }
    
        @Override
        public CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)
        {
            return wrap(super.exceptionally(fn));
        }
    
        @Override
        public CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action)
        {
            return wrap(super.whenComplete(action));
        }
    
        @Override
        public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action)
        {
            return wrap(super.whenCompleteAsync(action));
        }
    
        @Override
        public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action,
                                                      Executor executor)
        {
            return wrap(super.whenCompleteAsync(action, executor));
        }
    
        @Override
        public <U> CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn)
        {
            return wrap(super.handle(fn));
        }
    
        @Override
        public <U> CompletableFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn)
        {
            return wrap(super.handleAsync(fn));
        }
    
        @Override
        public <U> CompletableFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn,
                                                    Executor executor)
        {
            return wrap(super.handleAsync(fn, executor));
        }
    
        @Override
        public boolean complete(T value)
        {
            return delegate.complete(value);
        }
    
        @Override
        public boolean completeExceptionally(Throwable ex)
        {
            return delegate.completeExceptionally(ex);
        }
    }
    

    RunMode.java:

    /**
     * Operational modes.
     */
    public enum RunMode
    {
        /**
         * Optimized for debugging problems (extra runtime checks, logging of the program state).
         */
        DEBUG,
        /**
         * Optimized for maximum performance.
         */
        RELEASE
    }
    

    AsynchronousException.java

    /**
     * Thrown when an asynchronous operation fails. The stacktrace indicates who triggered the operation.
     */
    public final class AsynchronousException extends RuntimeException
    {
        private static final long serialVersionUID = 0L;
    
        public AsynchronousException()
        {
        }
    }
    

    Usage:

    DebugCompletableFuture.wrap(CompletableFuture.supplyAsync(this::expensiveOperation));
    

    Upside: you'll get relatively clean asynchronous stack traces.

    Downside: Constructing a new AsynchronousException every time a future is created is extremely expensive. Specifically, if you're generating a lot of futures, this generates a lot of garbage on the heap and the GC overhead becomes noticeable.

    I am still hopeful that someone will come up with a better-performing approach.

    0 讨论(0)
  • 2020-12-30 07:00

    This is probably due to the JVM update when it finds that the stack is exhausted of emitting the same log so it starts to omit it.

    And the solution is using -XX:-OmitStackTraceInFastThrow flag to prevent JVM from optimizing built-in exceptions stack trace.

    0 讨论(0)
提交回复
热议问题