简体   繁体   中英

How can I throw an exception within an execution of a reactive pipeline that contains the stack trace of the creator of the reactive pipeline?

If I run the following reactive pipeline:

        Mono.empty()
                .switchIfEmpty(Mono.defer(() -> Mono.error(new RuntimeException())))
                .block();

I get the stack trace of the reactive step ( switchIfEmpty ); but I am interested in the stack trace of the creator of the pipeline. The reactive stack trace portion is useless and I am actually interested in debugging the request that triggered the creation of the pipeline (which was sub-sequentially executed).

Even when the stack trace of the creator of the pipeline is sometimes revealed below the reactive stack elements, it is not a guarantee since I have seen pipelines with too many unexpected bifurcations that have lost the pipeline creator stack trace at worst. The best case scenarios are that the stack trace of interest is so buried under stack elements that it makes debugging almost impossible.

Since the pipeline I am creating will be used to do a round trip request over a network, I am okay with a solution that demands paying a maximum performance price equivalent to 1 order of magnitude below the cost of doing a network request round trip.

The solution is quite simple:

  1. Create the exception at the same time of creating the pipeline; but do not throw it. That exception will be constructed with the stack trace of interest.
  2. Build the pipeline using the constructed exception to be used if needed.

The following should work:

        Mono.empty()
                .switchIfEmpty(Mono.error(new RuntimeException()))
                .block();

This pipeline will be more expensive in performance terms; but will respect the OP's requirement of not adding more than a fraction of what a network request roundtrip costs.

The following test cases verify and illustrate (check out the sequence of print outs) the effectiveness of this technique to obtain the desired stack trace:

import org.junit.jupiter.api.Test;

import java.io.PrintWriter;
import java.io.StringWriter;

import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

public class MonoSwitchIfEmptyTest {

    @Test
    void whenRequestThrowsPrecompiledException_thenExpectDesiredStackTrace() {
        trySwitchIfEmptyWithDesiredStackTrace(createAppropriateSwitchIfEmptyMono(true), true);
    }

    @Test
    void whenRequestThrowsLiveException_thenExpectUndesiredStackTrace() {
        trySwitchIfEmptyWithDesiredStackTrace(createAppropriateSwitchIfEmptyMono(false), false);
    }

    // This is done here to force the creation of an extra stack element into the stack trace.
    // That stack element is the one that later will be checked to determine if the test passed or not.
    private Mono<Void> createAppropriateSwitchIfEmptyMono(boolean desiredStackTrace) {
        return desiredStackTrace ?
                Mono.error(new LoggedRuntimeException()) :
                Mono.defer(() -> Mono.error(new LoggedRuntimeException()));
    }

    private void trySwitchIfEmptyWithDesiredStackTrace(Mono<Void> switchIfEmptyMono, boolean desiredStackTrace) {
        Mono<Void> loggedError = Mono.fromRunnable(() -> System.out.println("Throwing exception..."))
                .then(switchIfEmptyMono);

        Mono<Object> testedPipeline = Mono.fromRunnable(() -> loggedBusyWork(1000))
                .switchIfEmpty(loggedError);

        StepVerifier.create(testedPipeline)
                .expectErrorMatches(e -> ((LoggedRuntimeException)e).isRelevantStackTrace() == desiredStackTrace)
                .verify();
    }

    private void loggedBusyWork(int millis) {
        long startTime = System.currentTimeMillis();
        System.out.println("Starting busy work @ " + startTime + "...");
        while (System.currentTimeMillis() - startTime < millis);
        System.out.println("End busy work @ " + System.currentTimeMillis());
    }

    static class LoggedRuntimeException extends RuntimeException {

        public LoggedRuntimeException() {
            System.out.println("Creating exception...");
            String stackTrace = getStackTraceStr();
            System.out.println("Stack trace: \n" + stackTrace);
        }

        private String getStackTraceStr() {
            StringWriter writer = new StringWriter();
            printStackTrace(new PrintWriter(writer));
            return writer.toString();
        }

        public boolean isRelevantStackTrace() {
            return getStackTrace()[1].toString().contains(MonoSwitchIfEmptyTest.class.getName());
        }
    }
}

Complete code on GitHub

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM