Skip to content

Commit 03ec765

Browse files
artembilanapp
authored and
app
committed
spring-projectsGH-82: Expose @CircuitBreaker.throwLastExceptionOnExhausted()
Fixes: spring-projects#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`
1 parent 44013e2 commit 03ec765

File tree

4 files changed

+40
-7
lines changed

4 files changed

+40
-7
lines changed

src/main/java/org/springframework/retry/annotation/AnnotationAwareRetryOperationsInterceptor.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2006-2023 the original author or authors.
2+
* Copyright 2006-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -252,6 +252,7 @@ private MethodInterceptor getStatefulInterceptor(Object target, Method method, R
252252
resetTimeout(breaker, circuit);
253253
template.setRetryPolicy(breaker);
254254
template.setBackOffPolicy(new NoBackOffPolicy());
255+
template.setThrowLastExceptionOnExhausted(circuit.throwLastExceptionOnExhausted());
255256
String label = circuit.label();
256257
if (!StringUtils.hasText(label)) {
257258
label = method.toGenericString();

src/main/java/org/springframework/retry/annotation/CircuitBreaker.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2022 the original author or authors.
2+
* Copyright 2016-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -181,4 +181,13 @@
181181
@AliasFor(annotation = Retryable.class)
182182
String exceptionExpression() default "";
183183

184+
/**
185+
* Set to {@code true} to not wrap last exception to the
186+
* {@link org.springframework.retry.ExhaustedRetryException} when retry is exhausted.
187+
* @return the boolean flag to whether wrap the last exception to the
188+
* {@link org.springframework.retry.ExhaustedRetryException}
189+
* @since 2.0.6
190+
*/
191+
boolean throwLastExceptionOnExhausted() default false;
192+
184193
}

src/main/java/org/springframework/retry/policy/ExpressionRetryPolicy.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ public boolean canRetry(RetryContext context) {
115115
return super.canRetry(context);
116116
}
117117
else {
118-
return super.canRetry(context)
119-
&& Boolean.TRUE.equals(this.expression.getValue(this.evaluationContext, lastThrowable, Boolean.class));
118+
return super.canRetry(context) && Boolean.TRUE
119+
.equals(this.expression.getValue(this.evaluationContext, lastThrowable, Boolean.class));
120120
}
121121
}
122122

src/test/java/org/springframework/retry/annotation/CircuitBreakerTests.java

+26-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2006-2023 the original author or authors.
2+
* Copyright 2006-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -29,6 +29,7 @@
2929
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
3030
import org.springframework.context.annotation.Bean;
3131
import org.springframework.context.annotation.Configuration;
32+
import org.springframework.retry.ExhaustedRetryException;
3233
import org.springframework.retry.RetryContext;
3334
import org.springframework.retry.policy.CircuitBreakerRetryPolicy;
3435
import org.springframework.retry.support.RetrySynchronizationManager;
@@ -106,15 +107,21 @@ void runtimeExpressions() throws Exception {
106107
assertThat(maxAttempts.get()).isEqualTo(10);
107108
CircuitBreakerRetryPolicy policy = TestUtils.getPropertyValue(interceptor, "retryOperations.retryPolicy",
108109
CircuitBreakerRetryPolicy.class);
109-
Supplier openTO = TestUtils.getPropertyValue(policy, "openTimeoutSupplier", Supplier.class);
110+
Supplier<?> openTO = TestUtils.getPropertyValue(policy, "openTimeoutSupplier", Supplier.class);
110111
assertThat(openTO).isNotNull();
111112
assertThat(openTO.get()).isEqualTo(10000L);
112-
Supplier resetTO = TestUtils.getPropertyValue(policy, "resetTimeoutSupplier", Supplier.class);
113+
Supplier<?> resetTO = TestUtils.getPropertyValue(policy, "resetTimeoutSupplier", Supplier.class);
113114
assertThat(resetTO).isNotNull();
114115
assertThat(resetTO.get()).isEqualTo(20000L);
115116
RetryContext ctx = service.getContext();
116117
assertThat(TestUtils.getPropertyValue(ctx, "openWindow")).isEqualTo(10000L);
117118
assertThat(TestUtils.getPropertyValue(ctx, "timeout")).isEqualTo(20000L);
119+
120+
assertThatExceptionOfType(ExhaustedRetryException.class).isThrownBy(service::exhaustedRetryService);
121+
122+
assertThatExceptionOfType(RuntimeException.class).isThrownBy(service::noWrapExhaustedRetryService)
123+
.withMessage("Planned");
124+
118125
context.close();
119126
}
120127

@@ -154,6 +161,10 @@ interface Service {
154161

155162
void expressionService3();
156163

164+
void exhaustedRetryService();
165+
166+
void noWrapExhaustedRetryService();
167+
157168
int getCount();
158169

159170
RetryContext getContext();
@@ -197,6 +208,18 @@ public void expressionService3() {
197208
this.count++;
198209
}
199210

211+
@Override
212+
@CircuitBreaker
213+
public void exhaustedRetryService() {
214+
throw new RuntimeException("Planned");
215+
}
216+
217+
@Override
218+
@CircuitBreaker(throwLastExceptionOnExhausted = true)
219+
public void noWrapExhaustedRetryService() {
220+
throw new RuntimeException("Planned");
221+
}
222+
200223
@Override
201224
public RetryContext getContext() {
202225
return this.context;

0 commit comments

Comments
 (0)