Skip to content

Commit

Permalink
wrap original exception when the circuit breaker is opened (#8222)
Browse files Browse the repository at this point in the history
- add throwWrappedException parameter to @CIRCUITBREAKER
  • Loading branch information
aprietop committed Nov 8, 2022
1 parent 4c5a19a commit ff30e35
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 4 deletions.
Expand Up @@ -95,4 +95,11 @@
*/
@AliasFor(annotation = Retryable.class, member = "predicate")
Class<? extends RetryPredicate> 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;
}
Expand Up @@ -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<CircuitState> state = new AtomicReference<>(CircuitState.CLOSED);
private volatile Throwable lastError;
private volatile long time = System.currentTimeMillis();
Expand All @@ -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
Expand All @@ -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);
Expand Down
Expand Up @@ -102,9 +102,12 @@ public Object intercept(MethodInvocationContext<Object, Object> 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();
Expand Down
Expand Up @@ -39,7 +39,7 @@ class CircuitBreakerRetrySpec extends Specification {
1000,
{->
new SimpleRetry(3, 2.0d, Duration.ofMillis(500))
}, null,null
}, null,null,false
)
retry.open()

Expand Down
Expand Up @@ -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
Expand Down Expand Up @@ -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 {

Expand Down Expand Up @@ -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
}
}
}

0 comments on commit ff30e35

Please sign in to comment.