Redesign RetryPolicy to directly incorporate BackOff

After experimenting with our newly introduced core retry support
(RetryPolicy, RetryTemplate, etc.) and @⁠Retryable support, it
became apparent that there are overlapping concerns between the current
RetryPolicy and BackOff contracts.

- RetryPolicy and BackOff both have stateful executions: RetryExecution
  and BackOffExecution. However, only one stateful execution is
  necessary.

- FixedBackOff and ExponentialBackOff already incorporate "retry" logic
  in terms of max attempts, max elapsed time, etc. Thus, there is no
  need to duplicate such behavior in a RetryPolicy and its
  RetryExecution.

- RetryTemplate currently accepts both a RetryPolicy and a BackOff in
  order to instrument the retry algorithm. However, users would
  probably rather focus on configuring all "retry" logic via a single
  mechanism.

In light of the above, this commit directly incorporates BackOff
in RetryPolicy as follows.

- Remove the RetryExecution interface and move its shouldRetry() method
  to RetryPolicy, replacing the current RetryExecution start() method.

- Introduce a default getBackOff() method in the RetryPolicy interface.

- Introduce RetryPolicy.withDefaults() factory method.

- Completely overhaul the RetryPolicy.Builder to provide support for
  configuring a BackOff strategy.

- Remove BackOff configuration from RetryTemplate.

- Revise the method signatures of callbacks in RetryListener.

The collective result of these changes can be witnessed in the
reworked implementation of AbstractRetryInterceptor.

RetryPolicy retryPolicy = RetryPolicy.builder()
		.includes(spec.includes())
		.excludes(spec.excludes())
		.predicate(spec.predicate().forMethod(method))
		.maxAttempts(spec.maxAttempts())
		.delay(Duration.ofMillis(spec.delay()))
		.maxDelay(Duration.ofMillis(spec.maxDelay()))
		.jitter(Duration.ofMillis(spec.jitter()))
		.multiplier(spec.multiplier())
		.build();

RetryTemplate retryTemplate = new RetryTemplate(retryPolicy);

See gh-34716
See gh-34529
See gh-35058
Closes gh-35110
This commit is contained in:
Sam Brannen
2025-06-27 17:28:10 +02:00
parent 5a6c019413
commit bcdf26d492
13 changed files with 994 additions and 527 deletions
@@ -34,7 +34,6 @@ import org.springframework.core.retry.RetryPolicy;
import org.springframework.core.retry.RetryTemplate;
import org.springframework.core.retry.Retryable;
import org.springframework.util.ClassUtils;
import org.springframework.util.backoff.ExponentialBackOff;
/**
* Abstract retry interceptor implementation, adapting a given
@@ -89,26 +88,17 @@ public abstract class AbstractRetryInterceptor implements MethodInterceptor {
}
}
RetryTemplate retryTemplate = new RetryTemplate();
RetryPolicy.Builder policyBuilder = RetryPolicy.builder();
for (Class<? extends Throwable> include : spec.includes()) {
policyBuilder.includes(include);
}
for (Class<? extends Throwable> exclude : spec.excludes()) {
policyBuilder.excludes(exclude);
}
policyBuilder.predicate(spec.predicate().forMethod(method));
policyBuilder.maxAttempts(spec.maxAttempts());
retryTemplate.setRetryPolicy(policyBuilder.build());
ExponentialBackOff backOff = new ExponentialBackOff();
backOff.setInitialInterval(spec.delay());
backOff.setJitter(spec.jitter());
backOff.setMultiplier(spec.multiplier());
backOff.setMaxInterval(spec.maxDelay());
backOff.setMaxAttempts(spec.maxAttempts());
retryTemplate.setBackOffPolicy(backOff);
RetryPolicy retryPolicy = RetryPolicy.builder()
.includes(spec.includes())
.excludes(spec.excludes())
.predicate(spec.predicate().forMethod(method))
.maxAttempts(spec.maxAttempts())
.delay(Duration.ofMillis(spec.delay()))
.maxDelay(Duration.ofMillis(spec.maxDelay()))
.jitter(Duration.ofMillis(spec.jitter()))
.multiplier(spec.multiplier())
.build();
RetryTemplate retryTemplate = new RetryTemplate(retryPolicy);
try {
return retryTemplate.execute(new Retryable<>() {