GH-82: Expose @CircuitBreaker.throwLastExceptionOnExhausted()

Fixes: #82

There are some use-cases when `ExhaustedRetryException` does not fit into the logic around Circuit Breaker pattern.

The `RetryTemplate` has already a `throwLastExceptionOnExhausted` flag for stateful retries

* Expose `@CircuitBreaker.throwLastExceptionOnExhausted()` and propagate it down to the `RetryTemplate`
in the `AnnotationAwareRetryOperationsInterceptor`
This commit is contained in:
Artem Bilan
2024-05-10 15:32:09 -04:00
parent 1c539502b7
commit d3698b8cf4
4 changed files with 40 additions and 7 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2006-2023 the original author or authors.
* Copyright 2006-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -252,6 +252,7 @@ public class AnnotationAwareRetryOperationsInterceptor implements IntroductionIn
resetTimeout(breaker, circuit);
template.setRetryPolicy(breaker);
template.setBackOffPolicy(new NoBackOffPolicy());
template.setThrowLastExceptionOnExhausted(circuit.throwLastExceptionOnExhausted());
String label = circuit.label();
if (!StringUtils.hasText(label)) {
label = method.toGenericString();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016-2022 the original author or authors.
* Copyright 2016-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -181,4 +181,13 @@ public @interface CircuitBreaker {
@AliasFor(annotation = Retryable.class)
String exceptionExpression() default "";
/**
* Set to {@code true} to not wrap last exception to the
* {@link org.springframework.retry.ExhaustedRetryException} when retry is exhausted.
* @return the boolean flag to whether wrap the last exception to the
* {@link org.springframework.retry.ExhaustedRetryException}
* @since 2.0.6
*/
boolean throwLastExceptionOnExhausted() default false;
}

View File

@@ -115,8 +115,8 @@ public class ExpressionRetryPolicy extends SimpleRetryPolicy implements BeanFact
return super.canRetry(context);
}
else {
return super.canRetry(context)
&& Boolean.TRUE.equals(this.expression.getValue(this.evaluationContext, lastThrowable, Boolean.class));
return super.canRetry(context) && Boolean.TRUE
.equals(this.expression.getValue(this.evaluationContext, lastThrowable, Boolean.class));
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2006-2023 the original author or authors.
* Copyright 2006-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -29,6 +29,7 @@ import org.springframework.beans.DirectFieldAccessor;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.ExhaustedRetryException;
import org.springframework.retry.RetryContext;
import org.springframework.retry.policy.CircuitBreakerRetryPolicy;
import org.springframework.retry.support.RetrySynchronizationManager;
@@ -106,15 +107,21 @@ public class CircuitBreakerTests {
assertThat(maxAttempts.get()).isEqualTo(10);
CircuitBreakerRetryPolicy policy = TestUtils.getPropertyValue(interceptor, "retryOperations.retryPolicy",
CircuitBreakerRetryPolicy.class);
Supplier openTO = TestUtils.getPropertyValue(policy, "openTimeoutSupplier", Supplier.class);
Supplier<?> openTO = TestUtils.getPropertyValue(policy, "openTimeoutSupplier", Supplier.class);
assertThat(openTO).isNotNull();
assertThat(openTO.get()).isEqualTo(10000L);
Supplier resetTO = TestUtils.getPropertyValue(policy, "resetTimeoutSupplier", Supplier.class);
Supplier<?> resetTO = TestUtils.getPropertyValue(policy, "resetTimeoutSupplier", Supplier.class);
assertThat(resetTO).isNotNull();
assertThat(resetTO.get()).isEqualTo(20000L);
RetryContext ctx = service.getContext();
assertThat(TestUtils.getPropertyValue(ctx, "openWindow")).isEqualTo(10000L);
assertThat(TestUtils.getPropertyValue(ctx, "timeout")).isEqualTo(20000L);
assertThatExceptionOfType(ExhaustedRetryException.class).isThrownBy(service::exhaustedRetryService);
assertThatExceptionOfType(RuntimeException.class).isThrownBy(service::noWrapExhaustedRetryService)
.withMessage("Planned");
context.close();
}
@@ -154,6 +161,10 @@ public class CircuitBreakerTests {
void expressionService3();
void exhaustedRetryService();
void noWrapExhaustedRetryService();
int getCount();
RetryContext getContext();
@@ -197,6 +208,18 @@ public class CircuitBreakerTests {
this.count++;
}
@Override
@CircuitBreaker
public void exhaustedRetryService() {
throw new RuntimeException("Planned");
}
@Override
@CircuitBreaker(throwLastExceptionOnExhausted = true)
public void noWrapExhaustedRetryService() {
throw new RuntimeException("Planned");
}
@Override
public RetryContext getContext() {
return this.context;