如何在包含反应管道创建者堆栈跟踪的反应管道执行中抛出异常?
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?
如果我 运行 以下反应管道:
Mono.empty()
.switchIfEmpty(Mono.defer(() -> Mono.error(new RuntimeException())))
.block();
我得到了反应步骤的堆栈跟踪(switchIfEmpty
);但我对管道创建者的堆栈跟踪感兴趣。 reactive stack trace 部分没用,我实际上对调试触发管道创建的请求感兴趣(这是顺序执行的).
即使管道创建者的堆栈跟踪有时会显示在反应性堆栈元素下方,也不能保证,因为我见过太多意外分叉的管道,最坏的情况是丢失了管道创建者堆栈跟踪.最好的情况是感兴趣的堆栈跟踪被隐藏在堆栈元素下,几乎不可能进行调试。
由于我正在创建的管道将用于通过网络执行往返请求,因此我可以接受要求支付最高性能价格的解决方案,该价格相当于执行网络成本的 1 个数量级请求往返。
解决方法很简单:
- 在创建管道的同时创建异常;但不要扔掉它。将使用感兴趣的堆栈跟踪构造该异常。
- 使用构建的异常构建管道,以便在需要时使用。
以下应该有效:
Mono.empty()
.switchIfEmpty(Mono.error(new RuntimeException()))
.block();
这条管道在性能方面会更昂贵;但会尊重 OP 的要求,即不增加超过网络请求往返成本的一小部分。
以下测试用例验证并说明(检查打印输出的顺序)此技术获得所需堆栈跟踪的有效性:
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());
}
}
}
如果我 运行 以下反应管道:
Mono.empty()
.switchIfEmpty(Mono.defer(() -> Mono.error(new RuntimeException())))
.block();
我得到了反应步骤的堆栈跟踪(switchIfEmpty
);但我对管道创建者的堆栈跟踪感兴趣。 reactive stack trace 部分没用,我实际上对调试触发管道创建的请求感兴趣(这是顺序执行的).
即使管道创建者的堆栈跟踪有时会显示在反应性堆栈元素下方,也不能保证,因为我见过太多意外分叉的管道,最坏的情况是丢失了管道创建者堆栈跟踪.最好的情况是感兴趣的堆栈跟踪被隐藏在堆栈元素下,几乎不可能进行调试。
由于我正在创建的管道将用于通过网络执行往返请求,因此我可以接受要求支付最高性能价格的解决方案,该价格相当于执行网络成本的 1 个数量级请求往返。
解决方法很简单:
- 在创建管道的同时创建异常;但不要扔掉它。将使用感兴趣的堆栈跟踪构造该异常。
- 使用构建的异常构建管道,以便在需要时使用。
以下应该有效:
Mono.empty()
.switchIfEmpty(Mono.error(new RuntimeException()))
.block();
这条管道在性能方面会更昂贵;但会尊重 OP 的要求,即不增加超过网络请求往返成本的一小部分。
以下测试用例验证并说明(检查打印输出的顺序)此技术获得所需堆栈跟踪的有效性:
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());
}
}
}