Skip to content

Commit

Permalink
spring-projectsGH-82: Expose `@CircuitBreaker.throwLastExceptionOnExh…
Browse files Browse the repository at this point in the history
…austed()`

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`
  • Loading branch information
artembilan authored and app committed May 20, 2024
1 parent 44013e2 commit 03ec765
Show file tree
Hide file tree
Showing 4 changed files with 40 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -252,6 +252,7 @@ private MethodInterceptor getStatefulInterceptor(Object target, Method method, R
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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -181,4 +181,13 @@
@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;

}
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ public boolean canRetry(RetryContext context) {
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));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -29,6 +29,7 @@
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;
Expand Down Expand Up @@ -106,15 +107,21 @@ void runtimeExpressions() throws Exception {
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();
}

Expand Down Expand Up @@ -154,6 +161,10 @@ interface Service {

void expressionService3();

void exhaustedRetryService();

void noWrapExhaustedRetryService();

int getCount();

RetryContext getContext();
Expand Down Expand Up @@ -197,6 +208,18 @@ public void expressionService3() {
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;
Expand Down

0 comments on commit 03ec765

Please sign in to comment.