Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wrap original exception when the circuit breaker is opened #8222

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
}
}
}