diff --git a/README.md b/README.md
index 62c184f3..170d5db2 100644
--- a/README.md
+++ b/README.md
@@ -588,7 +588,34 @@ Expressions can contain property placeholders, such as `#{${max.delay}}` or
- `exceptionExpression` is evaluated against the thrown exception as the `#root` object.
- `maxAttemptsExpression` and the `@BackOff` expression attributes are evaluated once,
during initialization. There is no root object for the evaluation but they can reference
-other beans in the context.
+other beans in the context
+
+Starting with version 2.0, expressions in `@Retryable`, `@CircuitBreaker`, and `BackOff` can be evaluated once, during application initialization, or at runtime.
+With earlier versions, evaluation was always performed during initialization (except for `Retryable.exceptionExpression` which is always evaluated at runtime).
+When evaluating at runtime, a root object containing the method arguments is passed to the evaluation context.
+
+**Note:** The arguments are not available until the method has been called at least once; they will be null initially, which means, for example, you can't set the initial `maxAttempts` using an argument value, you can, however, change the `maxAttempts` after the first failure and before any retries are performed.
+Also, the arguments are only available when using stateless retry (which includes the `@CircuitBreaker`).
+
+##### Examples
+
+```java
+@Retryable(maxAttemptsExpression = "@runtimeConfigs.maxAttempts",
+ backoff = @Backoff(delayExpression = "@runtimeConfigs.initial",
+ maxDelayExpression = "@runtimeConfigs.max", multiplierExpression = "@runtimeConfigs.mult"))
+public void service() {
+ ...
+}
+```
+
+Where `runtimeConfigs` is a bean with those properties.
+
+```java
+@Retryable(maxAttemptsExpression = "args[0] == 'foo' ? 3 : 1")
+public void conditional(String string) {
+ ...
+}
+```
#### Additional Dependencies
diff --git a/src/main/java/org/springframework/retry/annotation/AnnotationAwareRetryOperationsInterceptor.java b/src/main/java/org/springframework/retry/annotation/AnnotationAwareRetryOperationsInterceptor.java
index 74585b09..70a6cacb 100644
--- a/src/main/java/org/springframework/retry/annotation/AnnotationAwareRetryOperationsInterceptor.java
+++ b/src/main/java/org/springframework/retry/annotation/AnnotationAwareRetryOperationsInterceptor.java
@@ -40,9 +40,11 @@
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.expression.Expression;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
+import org.springframework.retry.RetryContext;
import org.springframework.retry.RetryListener;
import org.springframework.retry.RetryPolicy;
import org.springframework.retry.backoff.BackOffPolicy;
@@ -59,6 +61,8 @@
import org.springframework.retry.policy.MapRetryContextCache;
import org.springframework.retry.policy.RetryContextCache;
import org.springframework.retry.policy.SimpleRetryPolicy;
+import org.springframework.retry.support.Args;
+import org.springframework.retry.support.RetrySynchronizationManager;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ReflectionUtils;
@@ -211,8 +215,8 @@ private A findAnnotationOnTarget(Object target, Method me
private MethodInterceptor getStatelessInterceptor(Object target, Method method, Retryable retryable) {
RetryTemplate template = createTemplate(retryable.listeners());
- template.setRetryPolicy(getRetryPolicy(retryable));
- template.setBackOffPolicy(getBackoffPolicy(retryable.backoff()));
+ template.setRetryPolicy(getRetryPolicy(retryable, true));
+ template.setBackOffPolicy(getBackoffPolicy(retryable.backoff(), true));
return RetryInterceptorBuilder.stateless().retryOperations(template).label(retryable.label())
.recoverer(getRecoverer(target, method)).build();
}
@@ -226,10 +230,10 @@ private MethodInterceptor getStatefulInterceptor(Object target, Method method, R
circuit = findAnnotationOnTarget(target, method, CircuitBreaker.class);
}
if (circuit != null) {
- RetryPolicy policy = getRetryPolicy(circuit);
+ RetryPolicy policy = getRetryPolicy(circuit, false);
CircuitBreakerRetryPolicy breaker = new CircuitBreakerRetryPolicy(policy);
- breaker.setOpenTimeout(getOpenTimeout(circuit));
- breaker.setResetTimeout(getResetTimeout(circuit));
+ openTimeout(breaker, circuit);
+ resetTimeout(breaker, circuit);
template.setRetryPolicy(breaker);
template.setBackOffPolicy(new NoBackOffPolicy());
String label = circuit.label();
@@ -239,54 +243,50 @@ private MethodInterceptor getStatefulInterceptor(Object target, Method method, R
return RetryInterceptorBuilder.circuitBreaker().keyGenerator(new FixedKeyGenerator("circuit"))
.retryOperations(template).recoverer(getRecoverer(target, method)).label(label).build();
}
- RetryPolicy policy = getRetryPolicy(retryable);
+ RetryPolicy policy = getRetryPolicy(retryable, false);
template.setRetryPolicy(policy);
- template.setBackOffPolicy(getBackoffPolicy(retryable.backoff()));
+ template.setBackOffPolicy(getBackoffPolicy(retryable.backoff(), false));
String label = retryable.label();
return RetryInterceptorBuilder.stateful().keyGenerator(this.methodArgumentsKeyGenerator)
.newMethodArgumentsIdentifier(this.newMethodArgumentsIdentifier).retryOperations(template).label(label)
.recoverer(getRecoverer(target, method)).build();
}
- private long getOpenTimeout(CircuitBreaker circuit) {
- if (StringUtils.hasText(circuit.openTimeoutExpression())) {
- Long value = null;
- if (isTemplate(circuit.openTimeoutExpression())) {
- value = PARSER.parseExpression(resolve(circuit.openTimeoutExpression()), PARSER_CONTEXT)
- .getValue(this.evaluationContext, Long.class);
+ private void openTimeout(CircuitBreakerRetryPolicy breaker, CircuitBreaker circuit) {
+ String expression = circuit.openTimeoutExpression();
+ if (StringUtils.hasText(expression)) {
+ Expression parsed = parse(expression);
+ if (isTemplate(expression)) {
+ Long value = parsed.getValue(this.evaluationContext, Long.class);
+ if (value != null) {
+ breaker.setOpenTimeout(value);
+ return;
+ }
}
else {
- value = PARSER.parseExpression(resolve(circuit.openTimeoutExpression()))
- .getValue(this.evaluationContext, Long.class);
- }
- if (value != null) {
- return value;
+ breaker.setOpenTimeout(() -> evaluate(parsed, Long.class, false));
+ return;
}
}
- return circuit.openTimeout();
+ breaker.setOpenTimeout(circuit.openTimeout());
}
- private long getResetTimeout(CircuitBreaker circuit) {
- if (StringUtils.hasText(circuit.resetTimeoutExpression())) {
- Long value = null;
- if (isTemplate(circuit.openTimeoutExpression())) {
- value = PARSER.parseExpression(resolve(circuit.resetTimeoutExpression()), PARSER_CONTEXT)
- .getValue(this.evaluationContext, Long.class);
+ private void resetTimeout(CircuitBreakerRetryPolicy breaker, CircuitBreaker circuit) {
+ String expression = circuit.resetTimeoutExpression();
+ if (StringUtils.hasText(expression)) {
+ Expression parsed = parse(expression);
+ if (isTemplate(expression)) {
+ Long value = parsed.getValue(this.evaluationContext, Long.class);
+ if (value != null) {
+ breaker.setResetTimeout(value);
+ return;
+ }
}
else {
- value = PARSER.parseExpression(resolve(circuit.resetTimeoutExpression()))
- .getValue(this.evaluationContext, Long.class);
- }
- if (value != null) {
- return value;
+ breaker.setResetTimeout(() -> evaluate(parsed, Long.class, false));
}
}
- return circuit.resetTimeout();
- }
-
- private boolean isTemplate(String expression) {
- return expression.contains(PARSER_CONTEXT.getExpressionPrefix())
- && expression.contains(PARSER_CONTEXT.getExpressionSuffix());
+ breaker.setResetTimeout(circuit.resetTimeout());
}
private RetryTemplate createTemplate(String[] listenersBeanNames) {
@@ -325,7 +325,7 @@ private MethodInvocationRecoverer> getRecoverer(Object target, Method method)
return new RecoverAnnotationRecoveryHandler<>(target, method);
}
- private RetryPolicy getRetryPolicy(Annotation retryable) {
+ private RetryPolicy getRetryPolicy(Annotation retryable, boolean stateless) {
Map attrs = AnnotationUtils.getAnnotationAttributes(retryable);
@SuppressWarnings("unchecked")
Class extends Throwable>[] includes = (Class extends Throwable>[]) attrs.get("value");
@@ -340,21 +340,25 @@ private RetryPolicy getRetryPolicy(Annotation retryable) {
Class extends Throwable>[] excludes = (Class extends Throwable>[]) attrs.get("exclude");
Integer maxAttempts = (Integer) attrs.get("maxAttempts");
String maxAttemptsExpression = (String) attrs.get("maxAttemptsExpression");
+ Expression parsedExpression = null;
if (StringUtils.hasText(maxAttemptsExpression)) {
- if (ExpressionRetryPolicy.isTemplate(maxAttemptsExpression)) {
- maxAttempts = PARSER.parseExpression(resolve(maxAttemptsExpression), PARSER_CONTEXT)
- .getValue(this.evaluationContext, Integer.class);
- }
- else {
- maxAttempts = PARSER.parseExpression(resolve(maxAttemptsExpression)).getValue(this.evaluationContext,
- Integer.class);
+ parsedExpression = parse(maxAttemptsExpression);
+ if (isTemplate(maxAttemptsExpression)) {
+ maxAttempts = parsedExpression.getValue(this.evaluationContext, Integer.class);
+ parsedExpression = null;
}
}
+ final Expression expression = parsedExpression;
if (includes.length == 0 && excludes.length == 0) {
SimpleRetryPolicy simple = hasExpression
? new ExpressionRetryPolicy(resolve(exceptionExpression)).withBeanFactory(this.beanFactory)
: new SimpleRetryPolicy();
- simple.setMaxAttempts(maxAttempts);
+ if (expression != null) {
+ simple.setMaxAttempts(() -> evaluate(expression, Integer.class, stateless));
+ }
+ else {
+ simple.setMaxAttempts(maxAttempts);
+ }
return simple;
}
Map, Boolean> policyMap = new HashMap<>();
@@ -370,63 +374,121 @@ private RetryPolicy getRetryPolicy(Annotation retryable) {
.withBeanFactory(this.beanFactory);
}
else {
- return new SimpleRetryPolicy(maxAttempts, policyMap, true, retryNotExcluded);
+ SimpleRetryPolicy policy = new SimpleRetryPolicy(maxAttempts, policyMap, true, retryNotExcluded);
+ if (expression != null) {
+ policy.setMaxAttempts(() -> evaluate(expression, Integer.class, stateless));
+ }
+ return policy;
}
}
- private BackOffPolicy getBackoffPolicy(Backoff backoff) {
+ private BackOffPolicy getBackoffPolicy(Backoff backoff, boolean stateless) {
Map attrs = AnnotationUtils.getAnnotationAttributes(backoff);
long min = backoff.delay() == 0 ? backoff.value() : backoff.delay();
String delayExpression = (String) attrs.get("delayExpression");
+ Expression parsedMinExp = null;
if (StringUtils.hasText(delayExpression)) {
- if (ExpressionRetryPolicy.isTemplate(delayExpression)) {
- min = PARSER.parseExpression(resolve(delayExpression), PARSER_CONTEXT).getValue(this.evaluationContext,
- Long.class);
- }
- else {
- min = PARSER.parseExpression(resolve(delayExpression)).getValue(this.evaluationContext, Long.class);
+ parsedMinExp = parse(delayExpression);
+ if (isTemplate(delayExpression)) {
+ min = parsedMinExp.getValue(this.evaluationContext, Long.class);
+ parsedMinExp = null;
}
}
long max = backoff.maxDelay();
String maxDelayExpression = (String) attrs.get("maxDelayExpression");
+ Expression parsedMaxExp = null;
if (StringUtils.hasText(maxDelayExpression)) {
- if (ExpressionRetryPolicy.isTemplate(maxDelayExpression)) {
- max = PARSER.parseExpression(resolve(maxDelayExpression), PARSER_CONTEXT)
- .getValue(this.evaluationContext, Long.class);
- }
- else {
- max = PARSER.parseExpression(resolve(maxDelayExpression)).getValue(this.evaluationContext, Long.class);
+ parsedMaxExp = parse(maxDelayExpression);
+ if (isTemplate(maxDelayExpression)) {
+ max = parsedMaxExp.getValue(this.evaluationContext, Long.class);
+ parsedMaxExp = null;
}
}
double multiplier = backoff.multiplier();
String multiplierExpression = (String) attrs.get("multiplierExpression");
+ Expression parsedMultExp = null;
if (StringUtils.hasText(multiplierExpression)) {
- if (ExpressionRetryPolicy.isTemplate(multiplierExpression)) {
- multiplier = PARSER.parseExpression(resolve(multiplierExpression), PARSER_CONTEXT)
- .getValue(this.evaluationContext, Double.class);
- }
- else {
- multiplier = PARSER.parseExpression(resolve(multiplierExpression)).getValue(this.evaluationContext,
- Double.class);
+ parsedMultExp = parse(multiplierExpression);
+ if (isTemplate(multiplierExpression)) {
+ multiplier = parsedMultExp.getValue(this.evaluationContext, Double.class);
+ parsedMultExp = null;
}
}
boolean isRandom = false;
+ String randomExpression = (String) attrs.get("randomExpression");
+ Expression parsedRandomExp = null;
if (multiplier > 0) {
isRandom = backoff.random();
- String randomExpression = (String) attrs.get("randomExpression");
if (StringUtils.hasText(randomExpression)) {
- if (ExpressionRetryPolicy.isTemplate(randomExpression)) {
- isRandom = PARSER.parseExpression(resolve(randomExpression), PARSER_CONTEXT)
- .getValue(this.evaluationContext, Boolean.class);
- }
- else {
- isRandom = PARSER.parseExpression(resolve(randomExpression)).getValue(this.evaluationContext,
- Boolean.class);
+ parsedRandomExp = parse(randomExpression);
+ if (isTemplate(randomExpression)) {
+ isRandom = parsedRandomExp.getValue(this.evaluationContext, Boolean.class);
+ parsedRandomExp = null;
}
}
}
- return BackOffPolicyBuilder.newBuilder().delay(min).maxDelay(max).multiplier(multiplier).random(isRandom)
- .sleeper(this.sleeper).build();
+ return buildBackOff(min, parsedMinExp, max, parsedMaxExp, multiplier, parsedMultExp, isRandom, parsedRandomExp,
+ stateless);
+ }
+
+ private BackOffPolicy buildBackOff(long min, Expression minExp, long max, Expression maxExp, double multiplier,
+ Expression multExp, boolean isRandom, Expression randomExp, boolean stateless) {
+
+ BackOffPolicyBuilder builder = BackOffPolicyBuilder.newBuilder();
+ if (minExp != null) {
+ builder.delaySupplier(() -> evaluate(minExp, Long.class, stateless));
+ }
+ else {
+ builder.delay(min);
+ }
+ if (maxExp != null) {
+ builder.maxDelaySupplier(() -> evaluate(maxExp, Long.class, stateless));
+ }
+ else {
+ builder.maxDelay(max);
+ }
+ if (multExp != null) {
+ builder.multiplierSupplier(() -> evaluate(multExp, Double.class, stateless));
+ }
+ else {
+ builder.multiplier(multiplier);
+ }
+ if (randomExp != null) {
+ builder.randomSupplier(() -> evaluate(randomExp, Boolean.class, stateless));
+ }
+ else {
+ builder.random(isRandom);
+ }
+ builder.sleeper(this.sleeper);
+ return builder.build();
+ }
+
+ private Expression parse(String expression) {
+ if (isTemplate(expression)) {
+ return PARSER.parseExpression(resolve(expression), PARSER_CONTEXT);
+ }
+ else {
+ return PARSER.parseExpression(resolve(expression));
+ }
+ }
+
+ private boolean isTemplate(String expression) {
+ return expression.contains(PARSER_CONTEXT.getExpressionPrefix())
+ && expression.contains(PARSER_CONTEXT.getExpressionSuffix());
+ }
+
+ private T evaluate(Expression expression, Class type, boolean stateless) {
+ Args args = null;
+ if (stateless) {
+ RetryContext context = RetrySynchronizationManager.getContext();
+ if (context != null) {
+ args = (Args) context.getAttribute("ARGS");
+ }
+ if (args == null) {
+ args = Args.NO_ARGS;
+ }
+ }
+ return expression.getValue(this.evaluationContext, args, type);
}
/**
diff --git a/src/main/java/org/springframework/retry/annotation/Backoff.java b/src/main/java/org/springframework/retry/annotation/Backoff.java
index 6f786eef..17610de0 100644
--- a/src/main/java/org/springframework/retry/annotation/Backoff.java
+++ b/src/main/java/org/springframework/retry/annotation/Backoff.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2012-2013 the original author or authors.
+ * Copyright 2012-2022 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.
@@ -22,6 +22,7 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
+import org.springframework.core.annotation.AliasFor;
import org.springframework.retry.backoff.BackOffPolicy;
/**
@@ -53,6 +54,7 @@
* element is ignored, otherwise value of this element is taken.
* @return the delay in milliseconds (default 1000)
*/
+ @AliasFor("delay")
long value() default 1000;
/**
@@ -60,9 +62,10 @@
* as a minimum value in the uniform case. When the value of this element is 0, value
* of element {@link #value()} is taken, otherwise value of this element is taken and
* {@link #value()} is ignored.
- * @return the initial or canonical backoff period in milliseconds (default 0)
+ * @return the initial or canonical backoff period in milliseconds (default 1000)
*/
- long delay() default 0;
+ @AliasFor("value")
+ long delay() default 1000;
/**
* The maximum wait (in milliseconds) between retries. If less than the
@@ -83,7 +86,8 @@
/**
* An expression evaluating to the canonical backoff period. Used as an initial value
* in the exponential case, and as a minimum value in the uniform case. Overrides
- * {@link #delay()}.
+ * {@link #delay()}. Use {@code #{...}} for one-time evaluation during initialization,
+ * omit the delimiters for evaluation at runtime.
* @return the initial or canonical backoff period in milliseconds.
* @since 1.2
*/
@@ -93,7 +97,8 @@
* An expression evaluating to the maximum wait (in milliseconds) between retries. If
* less than the {@link #delay()} then the default of
* {@value org.springframework.retry.backoff.ExponentialBackOffPolicy#DEFAULT_MAX_INTERVAL}
- * is applied. Overrides {@link #maxDelay()}
+ * is applied. Overrides {@link #maxDelay()}. Use {@code #{...}} for one-time
+ * evaluation during initialization, omit the delimiters for evaluation at runtime.
* @return the maximum delay between retries (default 0 = ignored)
* @since 1.2
*/
@@ -101,7 +106,8 @@
/**
* Evaluates to a value used as a multiplier for generating the next delay for
- * backoff. Overrides {@link #multiplier()}.
+ * backoff. Overrides {@link #multiplier()}. Use {@code #{...}} for one-time
+ * evaluation during initialization, omit the delimiters for evaluation at runtime.
* @return a multiplier expression to use to calculate the next backoff delay (default
* 0 = ignored)
* @since 1.2
@@ -120,7 +126,8 @@
* Evaluates to a value. In the exponential case ({@link #multiplier()} > 0) set
* this to true to have the backoff delays randomized, so that the maximum delay is
* multiplier times the previous delay and the distribution is uniform between the two
- * values.
+ * values. Use {@code #{...}} for one-time evaluation during initialization, omit the
+ * delimiters for evaluation at runtime.
* @return the flag to signal randomization is required (default false)
*/
String randomExpression() default "";
diff --git a/src/main/java/org/springframework/retry/annotation/CircuitBreaker.java b/src/main/java/org/springframework/retry/annotation/CircuitBreaker.java
index a5c75be9..0df6a5a8 100644
--- a/src/main/java/org/springframework/retry/annotation/CircuitBreaker.java
+++ b/src/main/java/org/springframework/retry/annotation/CircuitBreaker.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016-2018 the original author or authors.
+ * Copyright 2016-2022 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.
@@ -66,7 +66,9 @@
/**
* @return an expression evaluated to the maximum number of attempts (including the
- * first failure), defaults to 3 Overrides {@link #maxAttempts()}.
+ * first failure), defaults to 3 Overrides {@link #maxAttempts()}. Use {@code #{...}}
+ * for one-time evaluation during initialization, omit the delimiters for evaluation
+ * at runtime.
* @since 1.2.3
*/
String maxAttemptsExpression() default "";
@@ -89,7 +91,8 @@
/**
* If the circuit is open for longer than this timeout then it resets on the next call
* to give the downstream component a chance to respond again. Overrides
- * {@link #resetTimeout()}.
+ * {@link #resetTimeout()}. Use {@code #{...}} for one-time evaluation during
+ * initialization, omit the delimiters for evaluation at runtime.
* @return the timeout before an open circuit is reset in milliseconds, no default.
* @since 1.2.3
*/
@@ -106,7 +109,8 @@
/**
* When {@link #maxAttempts()} failures are reached within this timeout, the circuit
* is opened automatically, preventing access to the downstream component. Overrides
- * {@link #openTimeout()}.
+ * {@link #openTimeout()}. Use {@code #{...}} for one-time evaluation during
+ * initialization, omit the delimiters for evaluation at runtime.
* @return the timeout before an closed circuit is opened in milliseconds, no default.
* @since 1.2.3
*/
diff --git a/src/main/java/org/springframework/retry/annotation/Retryable.java b/src/main/java/org/springframework/retry/annotation/Retryable.java
index 62c1d8f1..23c670ec 100644
--- a/src/main/java/org/springframework/retry/annotation/Retryable.java
+++ b/src/main/java/org/springframework/retry/annotation/Retryable.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2014-2019 the original author or authors.
+ * Copyright 2014-2022 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.
@@ -95,7 +95,9 @@
/**
* @return an expression evaluated to the maximum number of attempts (including the
- * first failure), defaults to 3 Overrides {@link #maxAttempts()}.
+ * first failure), defaults to 3 Overrides {@link #maxAttempts()}. Use {@code #{...}}
+ * for one-time evaluation during initialization, omit the delimiters for evaluation
+ * at runtime.
* @since 1.2
*/
String maxAttemptsExpression() default "";
diff --git a/src/main/java/org/springframework/retry/backoff/BackOffPolicyBuilder.java b/src/main/java/org/springframework/retry/backoff/BackOffPolicyBuilder.java
index b9ff49f7..9ffa1b0c 100644
--- a/src/main/java/org/springframework/retry/backoff/BackOffPolicyBuilder.java
+++ b/src/main/java/org/springframework/retry/backoff/BackOffPolicyBuilder.java
@@ -16,6 +16,8 @@
package org.springframework.retry.backoff;
+import java.util.function.Supplier;
+
/**
* Fluent API for creating a {@link BackOffPolicy} based on given attributes. The delay
* values are expressed in milliseconds. If any provided value is less than one, the
@@ -72,16 +74,24 @@ public class BackOffPolicyBuilder {
private static final long DEFAULT_INITIAL_DELAY = 1000L;
- private long delay = DEFAULT_INITIAL_DELAY;
+ private Long delay = DEFAULT_INITIAL_DELAY;
- private long maxDelay;
+ private Long maxDelay;
- private double multiplier;
+ private Double multiplier;
- private boolean random;
+ private Boolean random;
private Sleeper sleeper;
+ private Supplier delaySupplier;
+
+ private Supplier maxDelaySupplier;
+
+ private Supplier multiplierSupplier;
+
+ private Supplier randomSupplier;
+
private BackOffPolicyBuilder() {
}
@@ -156,36 +166,110 @@ public BackOffPolicyBuilder sleeper(Sleeper sleeper) {
return this;
}
+ /**
+ * Set a supplier for the delay.
+ * @param delaySupplier the supplier.
+ * @return this
+ * @since 2.0
+ */
+ public BackOffPolicyBuilder delaySupplier(Supplier delaySupplier) {
+ this.delaySupplier = delaySupplier;
+ return this;
+ }
+
+ /**
+ * Set a supplier for the max delay.
+ * @param maxDelaySupplier the supplier.
+ * @return this
+ * @since 2.0
+ */
+ public BackOffPolicyBuilder maxDelaySupplier(Supplier maxDelaySupplier) {
+ this.maxDelaySupplier = maxDelaySupplier;
+ return this;
+ }
+
+ /**
+ * Set a supplier for the multiplier.
+ * @param multiplierSupplier the supplier.
+ * @return this
+ * @since 2.0
+ */
+ public BackOffPolicyBuilder multiplierSupplier(Supplier multiplierSupplier) {
+ this.multiplierSupplier = multiplierSupplier;
+ return this;
+ }
+
+ /**
+ * Set a supplier for the random.
+ * @param randomSupplier the supplier.
+ * @return this
+ * @since 2.0
+ */
+ public BackOffPolicyBuilder randomSupplier(Supplier randomSupplier) {
+ this.randomSupplier = randomSupplier;
+ return this;
+ }
+
/**
* Builds the {@link BackOffPolicy} with the given parameters.
* @return the {@link BackOffPolicy} instance
*/
public BackOffPolicy build() {
- if (this.multiplier > 0) {
- ExponentialBackOffPolicy policy = new ExponentialBackOffPolicy();
- if (this.random) {
+ if (this.multiplier != null && this.multiplier > 0 || this.multiplierSupplier != null) {
+ ExponentialBackOffPolicy policy;
+ if (Boolean.TRUE.equals(this.random)) {
policy = new ExponentialRandomBackOffPolicy();
}
- policy.setInitialInterval(this.delay);
- policy.setMultiplier(this.multiplier);
- policy.setMaxInterval(
- this.maxDelay > this.delay ? this.maxDelay : ExponentialBackOffPolicy.DEFAULT_MAX_INTERVAL);
+ else {
+ policy = new ExponentialBackOffPolicy();
+ }
+ if (this.delay != null) {
+ policy.setInitialInterval(this.delay);
+ }
+ if (this.delaySupplier != null) {
+ policy.setInitialInterval(this.delaySupplier);
+ }
+ if (this.multiplier != null) {
+ policy.setMultiplier(this.multiplier);
+ }
+ if (this.multiplierSupplier != null) {
+ policy.setMultiplier(this.multiplierSupplier);
+ }
+ if (this.maxDelay != null && this.delay != null) {
+ policy.setMaxInterval(
+ this.maxDelay > this.delay ? this.maxDelay : ExponentialBackOffPolicy.DEFAULT_MAX_INTERVAL);
+ }
+ if (this.maxDelaySupplier != null) {
+ policy.setMaxInterval(this.maxDelaySupplier);
+ }
if (this.sleeper != null) {
policy.setSleeper(this.sleeper);
}
return policy;
}
- if (this.maxDelay > this.delay) {
+ if (this.maxDelay != null && this.delay != null && this.maxDelay > this.delay) {
UniformRandomBackOffPolicy policy = new UniformRandomBackOffPolicy();
- policy.setMinBackOffPeriod(this.delay);
- policy.setMaxBackOffPeriod(this.maxDelay);
+ if (this.delay != null) {
+ policy.setMinBackOffPeriod(this.delay);
+ }
+ if (this.delaySupplier != null) {
+ policy.setMinBackOffPeriod(this.delaySupplier);
+ }
+ if (this.maxDelay != null) {
+ policy.setMaxBackOffPeriod(this.maxDelay);
+ }
+ if (this.maxDelaySupplier != null) {
+ policy.setMaxBackOffPeriod(this.maxDelaySupplier);
+ }
if (this.sleeper != null) {
policy.setSleeper(this.sleeper);
}
return policy;
}
FixedBackOffPolicy policy = new FixedBackOffPolicy();
- policy.setBackOffPeriod(this.delay);
+ if (this.delay != null) {
+ policy.setBackOffPeriod(this.delay);
+ }
if (this.sleeper != null) {
policy.setSleeper(this.sleeper);
}
diff --git a/src/main/java/org/springframework/retry/backoff/ExponentialBackOffPolicy.java b/src/main/java/org/springframework/retry/backoff/ExponentialBackOffPolicy.java
index 5523b5bd..d20e37f4 100644
--- a/src/main/java/org/springframework/retry/backoff/ExponentialBackOffPolicy.java
+++ b/src/main/java/org/springframework/retry/backoff/ExponentialBackOffPolicy.java
@@ -16,10 +16,13 @@
package org.springframework.retry.backoff;
+import java.util.function.Supplier;
+
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.retry.RetryContext;
+import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
@@ -63,17 +66,32 @@ public class ExponentialBackOffPolicy implements SleepingBackOffPolicy initialIntervalSupplier;
+
+ /**
+ * The maximum value of the backoff period in milliseconds.
+ */
+ private Supplier maxIntervalSupplier;
+
+ /**
+ * The value to add to the backoff period for each retry attempt.
+ */
+ private Supplier multiplierSupplier;
private Sleeper sleeper = new ThreadWaitSleeper();
@@ -110,7 +128,7 @@ protected void cloneValues(ExponentialBackOffPolicy target) {
* @param initialInterval the initial interval
*/
public void setInitialInterval(long initialInterval) {
- this.initialInterval = (initialInterval > 1 ? initialInterval : 1);
+ this.initialInterval = initialInterval > 1 ? initialInterval : 1;
}
/**
@@ -119,7 +137,7 @@ public void setInitialInterval(long initialInterval) {
* @param multiplier the multiplier
*/
public void setMultiplier(double multiplier) {
- this.multiplier = (multiplier > 1.0 ? multiplier : 1.0);
+ this.multiplier = multiplier > 1.0 ? multiplier : 1.0;
}
/**
@@ -133,12 +151,59 @@ public void setMaxInterval(long maxInterval) {
this.maxInterval = maxInterval > 0 ? maxInterval : 1;
}
+ /**
+ * Set the initial sleep interval value. Default supplier supplies {@code 100}
+ * millisecond.
+ * @param initialIntervalSupplier the initial interval
+ * @since 2.0
+ */
+ public void setInitialInterval(Supplier initialIntervalSupplier) {
+ Assert.notNull(initialIntervalSupplier, "'initialIntervalSupplier' cannot be null");
+ this.initialIntervalSupplier = initialIntervalSupplier;
+ }
+
+ /**
+ * Set the multiplier value. Default supplier supplies '2.0
'. Hint: do
+ * not use values much in excess of 1.0 (or the backoff will get very long very fast).
+ * @param multiplierSupplier the multiplier
+ * @since 2.0
+ */
+ public void setMultiplier(Supplier multiplierSupplier) {
+ Assert.notNull(multiplierSupplier, "'multiplierSupplier' cannot be null");
+ this.multiplierSupplier = multiplierSupplier;
+ }
+
+ /**
+ * Setter for maximum back off period. Default is 30000 (30 seconds). the value will
+ * be reset to 1 if this method is called with a value less than 1. Set this to avoid
+ * infinite waits if backing off a large number of times (or if the multiplier is set
+ * too high).
+ * @param maxIntervalSupplier in milliseconds.
+ * @since 2.0
+ */
+ public void setMaxInterval(Supplier maxIntervalSupplier) {
+ Assert.notNull(maxIntervalSupplier, "'maxIntervalSupplier' cannot be null");
+ this.maxIntervalSupplier = maxIntervalSupplier;
+ }
+
+ protected Supplier getInitialIntervalSupplier() {
+ return initialIntervalSupplier;
+ }
+
+ protected Supplier getMaxIntervalSupplier() {
+ return maxIntervalSupplier;
+ }
+
+ protected Supplier getMultiplierSupplier() {
+ return multiplierSupplier;
+ }
+
/**
* The initial period to sleep on the first backoff.
* @return the initial interval
*/
public long getInitialInterval() {
- return this.initialInterval;
+ return this.initialIntervalSupplier != null ? this.initialIntervalSupplier.get() : this.initialInterval;
}
/**
@@ -146,7 +211,7 @@ public long getInitialInterval() {
* @return the maximum interval.
*/
public long getMaxInterval() {
- return this.maxInterval;
+ return this.maxIntervalSupplier != null ? this.maxIntervalSupplier.get() : this.maxInterval;
}
/**
@@ -154,7 +219,7 @@ public long getMaxInterval() {
* @return the multiplier in use
*/
public double getMultiplier() {
- return this.multiplier;
+ return this.multiplierSupplier != null ? this.multiplierSupplier.get() : this.multiplier;
}
/**
@@ -162,7 +227,8 @@ public double getMultiplier() {
*/
@Override
public BackOffContext start(RetryContext context) {
- return new ExponentialBackOffContext(this.initialInterval, this.multiplier, this.maxInterval);
+ return new ExponentialBackOffContext(this.initialInterval, this.multiplier, this.maxInterval,
+ this.initialIntervalSupplier, this.multiplierSupplier, this.maxIntervalSupplier);
}
/**
@@ -191,16 +257,28 @@ static class ExponentialBackOffContext implements BackOffContext {
private final long maxInterval;
- public ExponentialBackOffContext(long interval, double multiplier, long maxInterval) {
+ private Supplier intervalSupplier;
+
+ private Supplier multiplierSupplier;
+
+ private Supplier maxIntervalSupplier;
+
+ public ExponentialBackOffContext(long interval, double multiplier, long maxInterval,
+ Supplier intervalSupplier, Supplier multiplierSupplier,
+ Supplier maxIntervalSupplier) {
this.interval = interval;
this.multiplier = multiplier;
this.maxInterval = maxInterval;
+ this.intervalSupplier = intervalSupplier;
+ this.multiplierSupplier = multiplierSupplier;
+ this.maxIntervalSupplier = maxIntervalSupplier;
}
public synchronized long getSleepAndIncrement() {
- long sleep = this.interval;
- if (sleep > this.maxInterval) {
- sleep = this.maxInterval;
+ long sleep = getInterval();
+ long max = getMaxInterval();
+ if (sleep > max) {
+ sleep = max;
}
else {
this.interval = getNextInterval();
@@ -209,27 +287,27 @@ public synchronized long getSleepAndIncrement() {
}
protected long getNextInterval() {
- return (long) (this.interval * this.multiplier);
+ return (long) (this.interval * getMultiplier());
}
public double getMultiplier() {
- return this.multiplier;
+ return this.multiplierSupplier != null ? this.multiplierSupplier.get() : this.multiplier;
}
public long getInterval() {
- return this.interval;
+ return this.intervalSupplier != null ? this.intervalSupplier.get() : this.interval;
}
public long getMaxInterval() {
- return this.maxInterval;
+ return this.maxIntervalSupplier != null ? this.maxIntervalSupplier.get() : this.maxInterval;
}
}
@Override
public String toString() {
- return ClassUtils.getShortName(getClass()) + "[initialInterval=" + this.initialInterval + ", multiplier="
- + this.multiplier + ", maxInterval=" + this.maxInterval + "]";
+ return ClassUtils.getShortName(getClass()) + "[initialInterval=" + getInitialInterval() + ", multiplier="
+ + getMultiplier() + ", maxInterval=" + getMaxInterval() + "]";
}
}
diff --git a/src/main/java/org/springframework/retry/backoff/ExponentialRandomBackOffPolicy.java b/src/main/java/org/springframework/retry/backoff/ExponentialRandomBackOffPolicy.java
index 9199f9f3..70dc14f9 100644
--- a/src/main/java/org/springframework/retry/backoff/ExponentialRandomBackOffPolicy.java
+++ b/src/main/java/org/springframework/retry/backoff/ExponentialRandomBackOffPolicy.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2012 the original author or authors.
+ * Copyright 2006-2022 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.
@@ -16,9 +16,10 @@
package org.springframework.retry.backoff;
-import org.springframework.retry.RetryContext;
-
import java.util.Random;
+import java.util.function.Supplier;
+
+import org.springframework.retry.RetryContext;
/**
* Implementation of {@link org.springframework.retry.backoff.ExponentialBackOffPolicy}
@@ -52,7 +53,8 @@ public class ExponentialRandomBackOffPolicy extends ExponentialBackOffPolicy {
* seeded with this policies settings.
*/
public BackOffContext start(RetryContext context) {
- return new ExponentialRandomBackOffContext(getInitialInterval(), getMultiplier(), getMaxInterval());
+ return new ExponentialRandomBackOffContext(getInitialInterval(), getMultiplier(), getMaxInterval(),
+ getInitialIntervalSupplier(), getMultiplierSupplier(), getMaxIntervalSupplier());
}
protected ExponentialBackOffPolicy newInstance() {
@@ -63,8 +65,11 @@ static class ExponentialRandomBackOffContext extends ExponentialBackOffPolicy.Ex
private final Random r = new Random();
- public ExponentialRandomBackOffContext(long expSeed, double multiplier, long maxInterval) {
- super(expSeed, multiplier, maxInterval);
+ public ExponentialRandomBackOffContext(long expSeed, double multiplier, long maxInterval,
+ Supplier expSeedSupplier, Supplier multiplierSupplier,
+ Supplier maxIntervalSupplier) {
+
+ super(expSeed, multiplier, maxInterval, expSeedSupplier, multiplierSupplier, maxIntervalSupplier);
}
@Override
diff --git a/src/main/java/org/springframework/retry/backoff/FixedBackOffPolicy.java b/src/main/java/org/springframework/retry/backoff/FixedBackOffPolicy.java
index 290f090e..5837eb09 100644
--- a/src/main/java/org/springframework/retry/backoff/FixedBackOffPolicy.java
+++ b/src/main/java/org/springframework/retry/backoff/FixedBackOffPolicy.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2014 the original author or authors.
+ * Copyright 2006-2022 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.
@@ -16,6 +16,10 @@
package org.springframework.retry.backoff;
+import java.util.function.Supplier;
+
+import org.springframework.util.Assert;
+
/**
* Implementation of {@link BackOffPolicy} that pauses for a fixed period of time before
* continuing. A pause is implemented using {@link Sleeper#sleep(long)}.
@@ -38,7 +42,7 @@ public class FixedBackOffPolicy extends StatelessBackOffPolicy implements Sleepi
/**
* The back off period in milliseconds. Defaults to 1000ms.
*/
- private volatile long backOffPeriod = DEFAULT_BACK_OFF_PERIOD;
+ private Supplier backOffPeriod = () -> DEFAULT_BACK_OFF_PERIOD;
private Sleeper sleeper = new ThreadWaitSleeper();
@@ -62,7 +66,18 @@ public void setSleeper(Sleeper sleeper) {
* @param backOffPeriod the back off period
*/
public void setBackOffPeriod(long backOffPeriod) {
- this.backOffPeriod = (backOffPeriod > 0 ? backOffPeriod : 1);
+ this.backOffPeriod = () -> (backOffPeriod > 0 ? backOffPeriod : 1);
+ }
+
+ /**
+ * Set a supplier for the back off period in milliseconds. Cannot be < 1. Default
+ * supplier supplies 1000ms.
+ * @param backOffPeriodSupplier the back off period
+ * @since 2.0
+ */
+ public void setBackOffPeriod(Supplier backOffPeriodSupplier) {
+ Assert.notNull(backOffPeriodSupplier, "'backOffPeriodSupplier' cannot be null");
+ this.backOffPeriod = backOffPeriodSupplier;
}
/**
@@ -70,7 +85,7 @@ public void setBackOffPeriod(long backOffPeriod) {
* @return the backoff period
*/
public long getBackOffPeriod() {
- return backOffPeriod;
+ return this.backOffPeriod.get();
}
/**
@@ -79,7 +94,7 @@ public long getBackOffPeriod() {
*/
protected void doBackOff() throws BackOffInterruptedException {
try {
- sleeper.sleep(backOffPeriod);
+ sleeper.sleep(this.backOffPeriod.get());
}
catch (InterruptedException e) {
throw new BackOffInterruptedException("Thread interrupted while sleeping", e);
@@ -87,7 +102,7 @@ protected void doBackOff() throws BackOffInterruptedException {
}
public String toString() {
- return "FixedBackOffPolicy[backOffPeriod=" + backOffPeriod + "]";
+ return "FixedBackOffPolicy[backOffPeriod=" + this.backOffPeriod.get() + "]";
}
}
diff --git a/src/main/java/org/springframework/retry/backoff/UniformRandomBackOffPolicy.java b/src/main/java/org/springframework/retry/backoff/UniformRandomBackOffPolicy.java
index fe268a18..4534981b 100644
--- a/src/main/java/org/springframework/retry/backoff/UniformRandomBackOffPolicy.java
+++ b/src/main/java/org/springframework/retry/backoff/UniformRandomBackOffPolicy.java
@@ -17,6 +17,9 @@
package org.springframework.retry.backoff;
import java.util.Random;
+import java.util.function.Supplier;
+
+import org.springframework.util.Assert;
/**
* Implementation of {@link BackOffPolicy} that pauses for a random period of time before
@@ -43,9 +46,9 @@ public class UniformRandomBackOffPolicy extends StatelessBackOffPolicy
*/
private static final long DEFAULT_BACK_OFF_MAX_PERIOD = 1500L;
- private volatile long minBackOffPeriod = DEFAULT_BACK_OFF_MIN_PERIOD;
+ private Supplier minBackOffPeriod = () -> DEFAULT_BACK_OFF_MIN_PERIOD;
- private volatile long maxBackOffPeriod = DEFAULT_BACK_OFF_MAX_PERIOD;
+ private Supplier maxBackOffPeriod = () -> DEFAULT_BACK_OFF_MAX_PERIOD;
private final Random random = new Random(System.currentTimeMillis());
@@ -73,7 +76,18 @@ public void setSleeper(Sleeper sleeper) {
* @param backOffPeriod the backoff period
*/
public void setMinBackOffPeriod(long backOffPeriod) {
- this.minBackOffPeriod = (backOffPeriod > 0 ? backOffPeriod : 1);
+ this.minBackOffPeriod = () -> (backOffPeriod > 0 ? backOffPeriod : 1);
+ }
+
+ /**
+ * Set a supplier for the minimum back off period in milliseconds. Cannot be < 1.
+ * Default supplier supplies 500ms.
+ * @param backOffPeriodSupplier the backoff period
+ * @since 2.0
+ */
+ public void setMinBackOffPeriod(Supplier backOffPeriodSupplier) {
+ Assert.notNull(backOffPeriodSupplier, "'backOffPeriodSupplier' cannot be null");
+ this.minBackOffPeriod = backOffPeriodSupplier;
}
/**
@@ -81,7 +95,7 @@ public void setMinBackOffPeriod(long backOffPeriod) {
* @return the backoff period
*/
public long getMinBackOffPeriod() {
- return minBackOffPeriod;
+ return minBackOffPeriod.get();
}
/**
@@ -90,7 +104,18 @@ public long getMinBackOffPeriod() {
* @param backOffPeriod the back off period
*/
public void setMaxBackOffPeriod(long backOffPeriod) {
- this.maxBackOffPeriod = (backOffPeriod > 0 ? backOffPeriod : 1);
+ this.maxBackOffPeriod = () -> (backOffPeriod > 0 ? backOffPeriod : 1);
+ }
+
+ /**
+ * Set a supplier for the maximum back off period in milliseconds. Cannot be < 1.
+ * Default supplier supplies 1500ms.
+ * @param backOffPeriodSupplier the back off period
+ * @since 2.0
+ */
+ public void setMaxBackOffPeriod(Supplier backOffPeriodSupplier) {
+ Assert.notNull(backOffPeriodSupplier, "'backOffPeriodSupplier' cannot be null");
+ this.maxBackOffPeriod = backOffPeriodSupplier;
}
/**
@@ -98,7 +123,7 @@ public void setMaxBackOffPeriod(long backOffPeriod) {
* @return the backoff period
*/
public long getMaxBackOffPeriod() {
- return maxBackOffPeriod;
+ return maxBackOffPeriod.get();
}
/**
@@ -107,9 +132,10 @@ public long getMaxBackOffPeriod() {
*/
protected void doBackOff() throws BackOffInterruptedException {
try {
- long delta = maxBackOffPeriod == minBackOffPeriod ? 0
- : random.nextInt((int) (maxBackOffPeriod - minBackOffPeriod));
- sleeper.sleep(minBackOffPeriod + delta);
+ Long min = this.minBackOffPeriod.get();
+ long delta = this.maxBackOffPeriod.get() == this.minBackOffPeriod.get() ? 0
+ : this.random.nextInt((int) (this.maxBackOffPeriod.get() - min));
+ this.sleeper.sleep(min + delta);
}
catch (InterruptedException e) {
throw new BackOffInterruptedException("Thread interrupted while sleeping", e);
diff --git a/src/main/java/org/springframework/retry/interceptor/RetryOperationsInterceptor.java b/src/main/java/org/springframework/retry/interceptor/RetryOperationsInterceptor.java
index 38247179..449db329 100644
--- a/src/main/java/org/springframework/retry/interceptor/RetryOperationsInterceptor.java
+++ b/src/main/java/org/springframework/retry/interceptor/RetryOperationsInterceptor.java
@@ -26,6 +26,7 @@
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.RetryOperations;
+import org.springframework.retry.support.Args;
import org.springframework.retry.support.RetrySynchronizationManager;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.Assert;
@@ -87,6 +88,7 @@ public Object invoke(final MethodInvocation invocation) throws Throwable {
public Object doWithRetry(RetryContext context) throws Exception {
context.setAttribute(RetryContext.NAME, this.label);
+ context.setAttribute("ARGS", new Args(invocation.getArguments()));
/*
* If we don't copy the invocation carefully it won't keep a reference to
diff --git a/src/main/java/org/springframework/retry/policy/CircuitBreakerRetryPolicy.java b/src/main/java/org/springframework/retry/policy/CircuitBreakerRetryPolicy.java
index a8a90aba..faace4c8 100644
--- a/src/main/java/org/springframework/retry/policy/CircuitBreakerRetryPolicy.java
+++ b/src/main/java/org/springframework/retry/policy/CircuitBreakerRetryPolicy.java
@@ -17,6 +17,7 @@
package org.springframework.retry.policy;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -44,6 +45,10 @@ public class CircuitBreakerRetryPolicy implements RetryPolicy {
private long openTimeout = 5000;
+ private Supplier resetTimeoutSupplier;
+
+ private Supplier openTimeoutSupplier;
+
public CircuitBreakerRetryPolicy() {
this(new SimpleRetryPolicy());
}
@@ -61,6 +66,17 @@ public void setResetTimeout(long timeout) {
this.resetTimeout = timeout;
}
+ /**
+ * A supplier for the timeout for resetting circuit in milliseconds. After the circuit
+ * opens it will re-close after this time has elapsed and the context will be
+ * restarted.
+ * @param timeoutSupplier a supplier for the timeout to set in milliseconds
+ * @since 2.0
+ */
+ public void setResetTimeout(Supplier timeoutSupplier) {
+ this.resetTimeoutSupplier = timeoutSupplier;
+ }
+
/**
* Timeout for tripping the open circuit. If the delegate policy cannot retry and the
* time elapsed since the context was started is less than this window, then the
@@ -71,6 +87,17 @@ public void setOpenTimeout(long timeout) {
this.openTimeout = timeout;
}
+ /**
+ * A supplier for the Timeout for tripping the open circuit. If the delegate policy
+ * cannot retry and the time elapsed since the context was started is less than this
+ * window, then the circuit is opened.
+ * @param timeoutSupplier a supplier for the timeout to set in milliseconds
+ * @since 2.0
+ */
+ public void setOpenTimeout(Supplier timeoutSupplier) {
+ this.openTimeoutSupplier = timeoutSupplier;
+ }
+
@Override
public boolean canRetry(RetryContext context) {
CircuitBreakerRetryContext circuit = (CircuitBreakerRetryContext) context;
@@ -86,7 +113,15 @@ public boolean canRetry(RetryContext context) {
@Override
public RetryContext open(RetryContext parent) {
- return new CircuitBreakerRetryContext(parent, this.delegate, this.resetTimeout, this.openTimeout);
+ long resetTimeout = this.resetTimeout;
+ if (this.resetTimeoutSupplier != null) {
+ resetTimeout = this.resetTimeoutSupplier.get();
+ }
+ long openTimeout = this.openTimeout;
+ if (this.resetTimeoutSupplier != null) {
+ openTimeout = this.openTimeoutSupplier.get();
+ }
+ return new CircuitBreakerRetryContext(parent, this.delegate, resetTimeout, openTimeout);
}
@Override
diff --git a/src/main/java/org/springframework/retry/policy/SimpleRetryPolicy.java b/src/main/java/org/springframework/retry/policy/SimpleRetryPolicy.java
index 678c86f5..f93f771e 100644
--- a/src/main/java/org/springframework/retry/policy/SimpleRetryPolicy.java
+++ b/src/main/java/org/springframework/retry/policy/SimpleRetryPolicy.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2019 the original author or authors.
+ * Copyright 2006-2022 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.
@@ -17,11 +17,13 @@
package org.springframework.retry.policy;
import java.util.Map;
+import java.util.function.Supplier;
import org.springframework.classify.BinaryExceptionClassifier;
import org.springframework.retry.RetryContext;
import org.springframework.retry.RetryPolicy;
import org.springframework.retry.context.RetryContextSupport;
+import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
@@ -62,7 +64,9 @@ public class SimpleRetryPolicy implements RetryPolicy {
*/
public final static int DEFAULT_MAX_ATTEMPTS = 3;
- private volatile int maxAttempts;
+ private int maxAttempts;
+
+ private Supplier maxAttemptsSupplier;
private BinaryExceptionClassifier retryableClassifier = new BinaryExceptionClassifier(false);
@@ -149,11 +153,30 @@ public void setMaxAttempts(int maxAttempts) {
this.maxAttempts = maxAttempts;
}
+ /**
+ * Set a supplier for the number of attempts before retries are exhausted. Includes
+ * the initial attempt before the retries begin so, generally, will be {@code >= 1}.
+ * For example setting this property to 3 means 3 attempts total (initial + 2
+ * retries). IMPORTANT: This policy cannot be serialized when a max attempts supplier
+ * is provided. Serialization might be used by a distributed cache when using this
+ * policy in a {@code CircuitBreaker} context.
+ * @param maxAttemptsSupplier the maximum number of attempts including the initial
+ * attempt.
+ * @since 2.0
+ */
+ public void setMaxAttempts(Supplier maxAttemptsSupplier) {
+ Assert.notNull(maxAttemptsSupplier, "'maxAttemptsSupplier' cannot be null");
+ this.maxAttemptsSupplier = maxAttemptsSupplier;
+ }
+
/**
* The maximum number of attempts before failure.
* @return the maximum number of attempts
*/
public int getMaxAttempts() {
+ if (this.maxAttemptsSupplier != null) {
+ return this.maxAttemptsSupplier.get();
+ }
return this.maxAttempts;
}
@@ -218,7 +241,7 @@ private boolean retryForException(Throwable ex) {
@Override
public String toString() {
- return ClassUtils.getShortName(getClass()) + "[maxAttempts=" + this.maxAttempts + "]";
+ return ClassUtils.getShortName(getClass()) + "[maxAttempts=" + getMaxAttempts() + "]";
}
}
diff --git a/src/main/java/org/springframework/retry/support/Args.java b/src/main/java/org/springframework/retry/support/Args.java
new file mode 100644
index 00000000..af3e6509
--- /dev/null
+++ b/src/main/java/org/springframework/retry/support/Args.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2022 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.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.retry.support;
+
+/**
+ * A root object containing the method arguments to use in expression evaluation.
+ * IMPORTANT; the arguments are not available (will contain nulls) until after the first
+ * call to the retryable method; this is generally only an issue for the
+ * {@code maxAttempts}, meaning the arguments cannot be used to indicate
+ * {@code maxAttempts = 0}.
+ *
+ * @author Gary Russell
+ * @Since 2.0
+ */
+public class Args {
+
+ /**
+ * An empty {@link Args} with 100 null arguments.
+ */
+ public static final Args NO_ARGS = new Args(new Object[100]);
+
+ private final Object[] args;
+
+ public Args(Object[] args) {
+ this.args = args;
+ }
+
+ public Object[] getArgs() {
+ return args;
+ }
+
+}
diff --git a/src/test/java/org/springframework/retry/annotation/CircuitBreakerTests.java b/src/test/java/org/springframework/retry/annotation/CircuitBreakerTests.java
index 6547fb83..95ca1862 100644
--- a/src/test/java/org/springframework/retry/annotation/CircuitBreakerTests.java
+++ b/src/test/java/org/springframework/retry/annotation/CircuitBreakerTests.java
@@ -17,6 +17,7 @@
package org.springframework.retry.annotation;
import java.util.Map;
+import java.util.function.Supplier;
import org.aopalliance.intercept.MethodInterceptor;
import org.junit.jupiter.api.Test;
@@ -31,6 +32,7 @@
import org.springframework.retry.RetryContext;
import org.springframework.retry.policy.CircuitBreakerRetryPolicy;
import org.springframework.retry.support.RetrySynchronizationManager;
+import org.springframework.retry.util.test.TestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -83,6 +85,38 @@ public void vanilla() throws Exception {
context.close();
}
+ @Test
+ void runtimeExpressions() throws Exception {
+ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class);
+ Service service = context.getBean(Service.class);
+ assertThat(AopUtils.isAopProxy(service)).isTrue();
+ service.expressionService3();
+ assertThat(service.getCount()).isEqualTo(1);
+ Advised advised = (Advised) service;
+ Advisor advisor = advised.getAdvisors()[0];
+ Map, ?> delegates = (Map, ?>) new DirectFieldAccessor(advisor).getPropertyValue("advice.delegates");
+ assertThat(delegates).hasSize(1);
+ Map, ?> methodMap = (Map, ?>) delegates.values().iterator().next();
+ MethodInterceptor interceptor = (MethodInterceptor) methodMap
+ .get(Service.class.getDeclaredMethod("expressionService3"));
+ Supplier> maxAttempts = TestUtils.getPropertyValue(interceptor,
+ "retryOperations.retryPolicy.delegate.maxAttemptsSupplier", Supplier.class);
+ assertThat(maxAttempts).isNotNull();
+ assertThat(maxAttempts.get()).isEqualTo(10);
+ CircuitBreakerRetryPolicy policy = TestUtils.getPropertyValue(interceptor, "retryOperations.retryPolicy",
+ CircuitBreakerRetryPolicy.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);
+ 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);
+ context.close();
+ }
+
@Configuration
@EnableRetry
protected static class TestConfiguration {
@@ -117,6 +151,8 @@ interface Service {
void expressionService2();
+ void expressionService3();
+
int getCount();
RetryContext getContext();
@@ -125,9 +161,9 @@ interface Service {
protected static class ServiceImpl implements Service {
- private int count = 0;
+ int count = 0;
- private RetryContext context;
+ RetryContext context;
@Override
@CircuitBreaker(RuntimeException.class)
@@ -146,10 +182,18 @@ public void expressionService() {
this.count++;
}
+ @Override
+ @CircuitBreaker(maxAttemptsExpression = "#{@configs.maxAttempts}",
+ openTimeoutExpression = "#{@configs.openTimeout}", resetTimeoutExpression = "#{@configs.resetTimeout}")
+ public void expressionService2() {
+ this.count++;
+ }
+
@Override
@CircuitBreaker(maxAttemptsExpression = "@configs.maxAttempts", openTimeoutExpression = "@configs.openTimeout",
resetTimeoutExpression = "@configs.resetTimeout")
- public void expressionService2() {
+ public void expressionService3() {
+ this.context = RetrySynchronizationManager.getContext();
this.count++;
}
diff --git a/src/test/java/org/springframework/retry/annotation/EnableRetryTests.java b/src/test/java/org/springframework/retry/annotation/EnableRetryTests.java
index d3035c78..bae4aceb 100644
--- a/src/test/java/org/springframework/retry/annotation/EnableRetryTests.java
+++ b/src/test/java/org/springframework/retry/annotation/EnableRetryTests.java
@@ -45,8 +45,13 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+import static org.assertj.core.api.Assertions.setMaxStackTraceElementsDisplayed;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
/**
* @author Dave Syer
@@ -81,6 +86,11 @@ public void multipleMethods() {
assertThat(service.getCount()).isEqualTo(3);
service.other();
assertThat(service.getCount()).isEqualTo(4);
+ setMaxStackTraceElementsDisplayed(100);
+ assertThatIllegalArgumentException().isThrownBy(() -> service.conditional("foo"));
+ assertThat(service.getCount()).isEqualTo(7);
+ assertThatIllegalArgumentException().isThrownBy(() -> service.conditional("bar"));
+ assertThat(service.getCount()).isEqualTo(8);
context.close();
}
@@ -237,6 +247,38 @@ public void testExpression() throws Exception {
context.close();
}
+ @Test
+ void runtimeExpressions() throws Exception {
+ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class);
+ ExpressionService service = context.getBean(ExpressionService.class);
+ service.service6();
+ RuntimeConfigs runtime = context.getBean(RuntimeConfigs.class);
+ verify(runtime, times(5)).getMaxAttempts();
+ verify(runtime, times(2)).getInitial();
+ verify(runtime, times(2)).getMax();
+ verify(runtime, times(2)).getMult();
+
+ RetryConfiguration config = context.getBean(RetryConfiguration.class);
+ AnnotationAwareRetryOperationsInterceptor advice = (AnnotationAwareRetryOperationsInterceptor) new DirectFieldAccessor(
+ config).getPropertyValue("advice");
+ @SuppressWarnings("unchecked")
+ Map