How to create async stacktraces?

后端 未结 2 2098
余生分开走
余生分开走 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  the type of value returned by the future
     */
    public final class DebugCompletableFuture extends CompletableFuture
    {
        private static RunMode RUN_MODE = RunMode.DEBUG;
        private static final Set CLASS_PREFIXES_TO_REMOVE = ImmutableSet.of(DebugCompletableFuture.class.getName(),
            CompletableFuture.class.getName(), ThreadPoolExecutor.class.getName());
        private static final Set> EXCEPTIONS_TO_UNWRAP = ImmutableSet.of(AsynchronousException.class,
            CompletionException.class, ExecutionException.class);
        private final CompletableFuture delegate;
        private final AsynchronousException asyncStacktrace;
    
        /**
         * @param delegate the stage to delegate to
         * @throws NullPointerException if any of the arguments are null
         */
        private DebugCompletableFuture(CompletableFuture 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 newMethods = getMethodsInStacktrace(asyncStacktrace);
                if (!newMethods.isEmpty())
                {
                    Set 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 getMethodsInStacktrace(Throwable exception)
        {
            requireThat("exception", exception).isNotNull();
            Set 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      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  CompletableFuture wrap(CompletableFuture 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.
         * 

    * 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 elementFilter) { Throwable cause = exception.getCause(); if (cause != null) filterStacktrace(cause, elementFilter); for (Throwable suppressed : exception.getSuppressed()) filterStacktrace(suppressed, elementFilter); StackTraceElement[] elements = exception.getStackTrace(); List 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 CompletableFuture thenApply(Function fn) { return wrap(super.thenApply(fn)); } @Override public CompletableFuture thenApplyAsync(Function fn) { return wrap(super.thenApplyAsync(fn)); } @Override public CompletableFuture thenApplyAsync(Function fn, Executor executor) { return wrap(super.thenApplyAsync(fn, executor)); } @Override public CompletableFuture thenAccept(Consumer action) { return wrap(super.thenAccept(action)); } @Override public CompletableFuture thenAcceptAsync(Consumer action) { return wrap(super.thenAcceptAsync(action)); } @Override public CompletableFuture thenAcceptAsync(Consumer action, Executor executor) { return wrap(super.thenAcceptAsync(action, executor)); } @Override public CompletableFuture thenRun(Runnable action) { return wrap(super.thenRun(action)); } @Override public CompletableFuture thenRunAsync(Runnable action) { return wrap(super.thenRunAsync(action)); } @Override public CompletableFuture thenRunAsync(Runnable action, Executor executor) { return wrap(super.thenRunAsync(action, executor)); } @Override public CompletableFuture thenCombine(CompletionStage other, BiFunction fn) { return wrap(super.thenCombine(other, fn)); } @Override public CompletableFuture thenCombineAsync(CompletionStage other, BiFunction fn) { return wrap(super.thenCombineAsync(other, fn)); } @Override public CompletableFuture thenCombineAsync(CompletionStage other, BiFunction fn, Executor executor) { return wrap(super.thenCombineAsync(other, fn, executor)); } @Override public CompletableFuture thenAcceptBoth(CompletionStage other, BiConsumer action) { return wrap(super.thenAcceptBoth(other, action)); } @Override public CompletableFuture thenAcceptBothAsync(CompletionStage other, BiConsumer action) { return wrap(super.thenAcceptBothAsync(other, action)); } @Override public CompletableFuture thenAcceptBothAsync(CompletionStage other, BiConsumer action, Executor executor) { return wrap(super.thenAcceptBothAsync(other, action, executor)); } @Override public CompletableFuture runAfterBoth(CompletionStage other, Runnable action) { return wrap(super.runAfterBoth(other, action)); } @Override public CompletableFuture runAfterBothAsync(CompletionStage other, Runnable action) { return wrap(super.runAfterBothAsync(other, action)); } @Override public CompletableFuture runAfterBothAsync(CompletionStage other, Runnable action, Executor executor) { return wrap(super.runAfterBothAsync(other, action, executor)); } @Override public CompletableFuture applyToEither(CompletionStage other, Function fn) { return wrap(super.applyToEither(other, fn)); } @Override public CompletableFuture applyToEitherAsync(CompletionStage other, Function fn) { return wrap(super.applyToEitherAsync(other, fn)); } @Override public CompletableFuture applyToEitherAsync(CompletionStage other, Function fn, Executor executor) { return wrap(super.applyToEitherAsync(other, fn, executor)); } @Override public CompletableFuture acceptEither(CompletionStage other, Consumer action) { return wrap(super.acceptEither(other, action)); } @Override public CompletableFuture acceptEitherAsync(CompletionStage other, Consumer action) { return wrap(super.acceptEitherAsync(other, action)); } @Override public CompletableFuture acceptEitherAsync(CompletionStage other, Consumer action, Executor executor) { return wrap(super.acceptEitherAsync(other, action, executor)); } @Override public CompletableFuture runAfterEither(CompletionStage other, Runnable action) { return wrap(super.runAfterEither(other, action)); } @Override public CompletableFuture runAfterEitherAsync(CompletionStage other, Runnable action) { return wrap(super.runAfterEitherAsync(other, action)); } @Override public CompletableFuture runAfterEitherAsync(CompletionStage other, Runnable action, Executor executor) { return wrap(super.runAfterEitherAsync(other, action, executor)); } @Override public CompletableFuture thenCompose(Function> fn) { return wrap(super.thenCompose(fn)); } @Override public CompletableFuture thenComposeAsync(Function> fn) { return wrap(super.thenComposeAsync(fn)); } @Override public CompletableFuture thenComposeAsync(Function> fn, Executor executor) { return wrap(super.thenComposeAsync(fn, executor)); } @Override public CompletableFuture exceptionally(Function fn) { return wrap(super.exceptionally(fn)); } @Override public CompletableFuture whenComplete(BiConsumer action) { return wrap(super.whenComplete(action)); } @Override public CompletableFuture whenCompleteAsync(BiConsumer action) { return wrap(super.whenCompleteAsync(action)); } @Override public CompletableFuture whenCompleteAsync(BiConsumer action, Executor executor) { return wrap(super.whenCompleteAsync(action, executor)); } @Override public CompletableFuture handle(BiFunction fn) { return wrap(super.handle(fn)); } @Override public CompletableFuture handleAsync(BiFunction fn) { return wrap(super.handleAsync(fn)); } @Override public CompletableFuture handleAsync(BiFunction 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.

提交回复
热议问题