diff --git a/runtime/src/main/java/io/micronaut/retry/annotation/CircuitBreaker.java b/runtime/src/main/java/io/micronaut/retry/annotation/CircuitBreaker.java index a59d86b800b..e3186977fda 100644 --- a/runtime/src/main/java/io/micronaut/retry/annotation/CircuitBreaker.java +++ b/runtime/src/main/java/io/micronaut/retry/annotation/CircuitBreaker.java @@ -95,4 +95,11 @@ */ @AliasFor(annotation = Retryable.class, member = "predicate") Class predicate() default DefaultRetryPredicate.class; + + /** + * If {@code true} and the circuit is opened, it throws the original exception wrapped + * in a {@link io.micronaut.retry.exception.CircuitOpenException} + * @return Whether to wrap the original exception in a {@link io.micronaut.retry.exception.CircuitOpenException} + */ + boolean throwWrappedException() default false; } diff --git a/runtime/src/main/java/io/micronaut/retry/intercept/CircuitBreakerRetry.java b/runtime/src/main/java/io/micronaut/retry/intercept/CircuitBreakerRetry.java index 9935fb8e54e..95426f8450f 100644 --- a/runtime/src/main/java/io/micronaut/retry/intercept/CircuitBreakerRetry.java +++ b/runtime/src/main/java/io/micronaut/retry/intercept/CircuitBreakerRetry.java @@ -46,6 +46,7 @@ class CircuitBreakerRetry implements MutableRetryState { private final long openTimeout; private final ExecutableMethod method; private final ApplicationEventPublisher eventPublisher; + private final boolean throwWrappedException; private AtomicReference state = new AtomicReference<>(CircuitState.CLOSED); private volatile Throwable lastError; private volatile long time = System.currentTimeMillis(); @@ -56,18 +57,20 @@ class CircuitBreakerRetry implements MutableRetryState { * @param childStateBuilder The retry state builder * @param method A compile time produced invocation of a method call * @param eventPublisher To publish circuit events + * @param throwWrappedException If {@code true}, the original exception will be wrapped in {@link CircuitOpenException} */ CircuitBreakerRetry( long openTimeout, RetryStateBuilder childStateBuilder, ExecutableMethod method, - ApplicationEventPublisher eventPublisher) { + ApplicationEventPublisher eventPublisher, boolean throwWrappedException) { this.retryStateBuilder = childStateBuilder; this.openTimeout = openTimeout; this.childState = (MutableRetryState) childStateBuilder.build(); this.eventPublisher = eventPublisher; this.method = method; + this.throwWrappedException = throwWrappedException; } @Override @@ -92,7 +95,7 @@ public void open() { if (LOG.isDebugEnabled()) { LOG.debug("Rethrowing existing exception for Open Circuit [{}]: {}", method, lastError.getMessage()); } - if (lastError instanceof RuntimeException) { + if (lastError instanceof RuntimeException && !throwWrappedException) { throw (RuntimeException) lastError; } else { throw new CircuitOpenException("Circuit Open: " + lastError.getMessage(), lastError); diff --git a/runtime/src/main/java/io/micronaut/retry/intercept/DefaultRetryInterceptor.java b/runtime/src/main/java/io/micronaut/retry/intercept/DefaultRetryInterceptor.java index 46758f4a421..a9faaf60a28 100644 --- a/runtime/src/main/java/io/micronaut/retry/intercept/DefaultRetryInterceptor.java +++ b/runtime/src/main/java/io/micronaut/retry/intercept/DefaultRetryInterceptor.java @@ -102,9 +102,12 @@ public Object intercept(MethodInvocationContext context) { long timeout = context .getValue(CircuitBreaker.class, "reset", Duration.class) .map(Duration::toMillis).orElse(Duration.ofSeconds(DEFAULT_CIRCUIT_BREAKER_TIMEOUT_IN_MILLIS).toMillis()); + boolean wrapException = context + .getValue(CircuitBreaker.class, "throwWrappedException", Boolean.class) + .orElse(false); retryState = circuitContexts.computeIfAbsent( context.getExecutableMethod(), - method -> new CircuitBreakerRetry(timeout, retryStateBuilder, context, eventPublisher) + method -> new CircuitBreakerRetry(timeout, retryStateBuilder, context, eventPublisher, wrapException) ); } else { retryState = (MutableRetryState) retryStateBuilder.build(); diff --git a/runtime/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerRetrySpec.groovy b/runtime/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerRetrySpec.groovy index 84078a81355..dd71b0c8ba5 100644 --- a/runtime/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerRetrySpec.groovy +++ b/runtime/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerRetrySpec.groovy @@ -39,7 +39,7 @@ class CircuitBreakerRetrySpec extends Specification { 1000, {-> new SimpleRetry(3, 2.0d, Duration.ofMillis(500)) - }, null,null + }, null,null,false ) retry.open() diff --git a/runtime/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerSpec.groovy b/runtime/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerSpec.groovy index 1a515c56627..b2de0579e72 100644 --- a/runtime/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerSpec.groovy +++ b/runtime/src/test/groovy/io/micronaut/retry/intercept/CircuitBreakerSpec.groovy @@ -22,6 +22,7 @@ import io.micronaut.retry.event.CircuitClosedEvent import io.micronaut.retry.event.CircuitOpenEvent import io.micronaut.retry.event.RetryEvent import io.micronaut.retry.event.RetryEventListener +import io.micronaut.retry.exception.CircuitOpenException import jakarta.inject.Singleton import org.reactivestreams.Publisher import reactor.core.publisher.Mono @@ -188,6 +189,34 @@ class CircuitBreakerSpec extends Specification{ result == 2 } + void "test circuit breaker throws a wrapped exception"(){ + given: + ApplicationContext context = ApplicationContext.run() + WrappedExceptionService service = context.getBean(WrappedExceptionService) + + when:"A method is annotated retry" + int result = service.getCount() + + then:"It executes until successful" + result == 2 + + when:"The threshold can never be met" + service.countThreshold = Integer.MAX_VALUE + service.countValue = 0 + service.getCount() + + then:"Throws the original exception" + thrown(IllegalStateException) + + when:"the method is called again" + service.getCount() + + then:"Throws the wrapped exception, the original logic is never invoked" + CircuitOpenException e = thrown() + e.getCause().getClass() == IllegalStateException + + } + @Singleton static class MyRetryListener implements RetryEventListener { @@ -268,4 +297,19 @@ class CircuitBreakerSpec extends Specification{ return countValue } } + + @Singleton + @CircuitBreaker(throwWrappedException = true) + static class WrappedExceptionService { + int countValue = 0 + int countThreshold = 2 + + int getCount() { + countValue++ + if(countValue < countThreshold) { + throw new IllegalStateException("Bad count") + } + return countValue + } + } }