GH-184: Expression Evaluation at Runtime

Resolves https://github.com/spring-projects/spring-retry/issues/184

* Remove `args` property when using stateful (including CircuitBreaker).

Other polishing from review.

* Docs for stateful.
This commit is contained in:
Gary Russell
2022-05-04 15:21:29 -04:00
committed by GitHub
parent 7a86c9eb98
commit 47c1e52900
17 changed files with 707 additions and 156 deletions

View File

@@ -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) {
...
}
```
#### <a name="Additional_Dependencies"></a> Additional Dependencies

View File

@@ -40,9 +40,11 @@ import org.springframework.context.expression.BeanFactoryResolver;
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.ExpressionRetryPolicy;
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 @@ public class AnnotationAwareRetryOperationsInterceptor implements IntroductionIn
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 @@ public class AnnotationAwareRetryOperationsInterceptor implements IntroductionIn
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 @@ public class AnnotationAwareRetryOperationsInterceptor implements IntroductionIn
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 @@ public class AnnotationAwareRetryOperationsInterceptor implements IntroductionIn
return new RecoverAnnotationRecoveryHandler<>(target, method);
}
private RetryPolicy getRetryPolicy(Annotation retryable) {
private RetryPolicy getRetryPolicy(Annotation retryable, boolean stateless) {
Map<String, Object> attrs = AnnotationUtils.getAnnotationAttributes(retryable);
@SuppressWarnings("unchecked")
Class<? extends Throwable>[] includes = (Class<? extends Throwable>[]) attrs.get("value");
@@ -340,21 +340,25 @@ public class AnnotationAwareRetryOperationsInterceptor implements IntroductionIn
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<Class<? extends Throwable>, Boolean> policyMap = new HashMap<>();
@@ -370,63 +374,121 @@ public class AnnotationAwareRetryOperationsInterceptor implements IntroductionIn
.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<String, Object> 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> T evaluate(Expression expression, Class<T> 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);
}
/**

View File

@@ -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.Retention;
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 @@ public @interface Backoff {
* 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 @@ public @interface Backoff {
* 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 @@ public @interface Backoff {
/**
* 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 @@ public @interface Backoff {
* 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 @@ public @interface Backoff {
/**
* 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 @@ public @interface Backoff {
* Evaluates to a value. In the exponential case ({@link #multiplier()} &gt; 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 "";

View File

@@ -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 @@ public @interface CircuitBreaker {
/**
* @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 @@ public @interface CircuitBreaker {
/**
* 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 @@ public @interface CircuitBreaker {
/**
* 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
*/

View File

@@ -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 @@ public @interface Retryable {
/**
* @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 "";

View File

@@ -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<Long> delaySupplier;
private Supplier<Long> maxDelaySupplier;
private Supplier<Double> multiplierSupplier;
private Supplier<Boolean> randomSupplier;
private BackOffPolicyBuilder() {
}
@@ -156,36 +166,110 @@ public class BackOffPolicyBuilder {
return this;
}
/**
* Set a supplier for the delay.
* @param delaySupplier the supplier.
* @return this
* @since 2.0
*/
public BackOffPolicyBuilder delaySupplier(Supplier<Long> 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<Long> 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<Double> 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<Boolean> 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);
}

View File

@@ -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<Exponenti
/**
* The initial backoff interval.
*/
private volatile long initialInterval = DEFAULT_INITIAL_INTERVAL;
private long initialInterval = DEFAULT_INITIAL_INTERVAL;
/**
* The maximum value of the backoff period in milliseconds.
*/
private volatile long maxInterval = DEFAULT_MAX_INTERVAL;
private long maxInterval = DEFAULT_MAX_INTERVAL;
/**
* The value to add to the backoff period for each retry attempt.
*/
private volatile double multiplier = DEFAULT_MULTIPLIER;
private double multiplier = DEFAULT_MULTIPLIER;
/**
* The initial backoff interval.
*/
private Supplier<Long> initialIntervalSupplier;
/**
* The maximum value of the backoff period in milliseconds.
*/
private Supplier<Long> maxIntervalSupplier;
/**
* The value to add to the backoff period for each retry attempt.
*/
private Supplier<Double> multiplierSupplier;
private Sleeper sleeper = new ThreadWaitSleeper();
@@ -110,7 +128,7 @@ public class ExponentialBackOffPolicy implements SleepingBackOffPolicy<Exponenti
* @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 class ExponentialBackOffPolicy implements SleepingBackOffPolicy<Exponenti
* @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 class ExponentialBackOffPolicy implements SleepingBackOffPolicy<Exponenti
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<Long> initialIntervalSupplier) {
Assert.notNull(initialIntervalSupplier, "'initialIntervalSupplier' cannot be null");
this.initialIntervalSupplier = initialIntervalSupplier;
}
/**
* Set the multiplier value. Default supplier supplies '<code>2.0</code>'. 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<Double> 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<Long> maxIntervalSupplier) {
Assert.notNull(maxIntervalSupplier, "'maxIntervalSupplier' cannot be null");
this.maxIntervalSupplier = maxIntervalSupplier;
}
protected Supplier<Long> getInitialIntervalSupplier() {
return initialIntervalSupplier;
}
protected Supplier<Long> getMaxIntervalSupplier() {
return maxIntervalSupplier;
}
protected Supplier<Double> 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 class ExponentialBackOffPolicy implements SleepingBackOffPolicy<Exponenti
* @return the maximum interval.
*/
public long getMaxInterval() {
return this.maxInterval;
return this.maxIntervalSupplier != null ? this.maxIntervalSupplier.get() : this.maxInterval;
}
/**
@@ -154,7 +219,7 @@ public class ExponentialBackOffPolicy implements SleepingBackOffPolicy<Exponenti
* @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 class ExponentialBackOffPolicy implements SleepingBackOffPolicy<Exponenti
*/
@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 @@ public class ExponentialBackOffPolicy implements SleepingBackOffPolicy<Exponenti
private final long maxInterval;
public ExponentialBackOffContext(long interval, double multiplier, long maxInterval) {
private Supplier<Long> intervalSupplier;
private Supplier<Double> multiplierSupplier;
private Supplier<Long> maxIntervalSupplier;
public ExponentialBackOffContext(long interval, double multiplier, long maxInterval,
Supplier<Long> intervalSupplier, Supplier<Double> multiplierSupplier,
Supplier<Long> 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 class ExponentialBackOffPolicy implements SleepingBackOffPolicy<Exponenti
}
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() + "]";
}
}

View File

@@ -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 @@ public class ExponentialRandomBackOffPolicy extends ExponentialBackOffPolicy {
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<Long> expSeedSupplier, Supplier<Double> multiplierSupplier,
Supplier<Long> maxIntervalSupplier) {
super(expSeed, multiplier, maxInterval, expSeedSupplier, multiplierSupplier, maxIntervalSupplier);
}
@Override

View File

@@ -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<Long> backOffPeriod = () -> DEFAULT_BACK_OFF_PERIOD;
private Sleeper sleeper = new ThreadWaitSleeper();
@@ -62,7 +66,18 @@ public class FixedBackOffPolicy extends StatelessBackOffPolicy implements Sleepi
* @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 &lt; 1. Default
* supplier supplies 1000ms.
* @param backOffPeriodSupplier the back off period
* @since 2.0
*/
public void setBackOffPeriod(Supplier<Long> backOffPeriodSupplier) {
Assert.notNull(backOffPeriodSupplier, "'backOffPeriodSupplier' cannot be null");
this.backOffPeriod = backOffPeriodSupplier;
}
/**
@@ -70,7 +85,7 @@ public class FixedBackOffPolicy extends StatelessBackOffPolicy implements Sleepi
* @return the backoff period
*/
public long getBackOffPeriod() {
return backOffPeriod;
return this.backOffPeriod.get();
}
/**
@@ -79,7 +94,7 @@ public class FixedBackOffPolicy extends StatelessBackOffPolicy implements Sleepi
*/
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 @@ public class FixedBackOffPolicy extends StatelessBackOffPolicy implements Sleepi
}
public String toString() {
return "FixedBackOffPolicy[backOffPeriod=" + backOffPeriod + "]";
return "FixedBackOffPolicy[backOffPeriod=" + this.backOffPeriod.get() + "]";
}
}

View File

@@ -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<Long> minBackOffPeriod = () -> DEFAULT_BACK_OFF_MIN_PERIOD;
private volatile long maxBackOffPeriod = DEFAULT_BACK_OFF_MAX_PERIOD;
private Supplier<Long> maxBackOffPeriod = () -> DEFAULT_BACK_OFF_MAX_PERIOD;
private final Random random = new Random(System.currentTimeMillis());
@@ -73,7 +76,18 @@ public class UniformRandomBackOffPolicy extends StatelessBackOffPolicy
* @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 &lt; 1.
* Default supplier supplies 500ms.
* @param backOffPeriodSupplier the backoff period
* @since 2.0
*/
public void setMinBackOffPeriod(Supplier<Long> backOffPeriodSupplier) {
Assert.notNull(backOffPeriodSupplier, "'backOffPeriodSupplier' cannot be null");
this.minBackOffPeriod = backOffPeriodSupplier;
}
/**
@@ -81,7 +95,7 @@ public class UniformRandomBackOffPolicy extends StatelessBackOffPolicy
* @return the backoff period
*/
public long getMinBackOffPeriod() {
return minBackOffPeriod;
return minBackOffPeriod.get();
}
/**
@@ -90,7 +104,18 @@ public class UniformRandomBackOffPolicy extends StatelessBackOffPolicy
* @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 &lt; 1.
* Default supplier supplies 1500ms.
* @param backOffPeriodSupplier the back off period
* @since 2.0
*/
public void setMaxBackOffPeriod(Supplier<Long> backOffPeriodSupplier) {
Assert.notNull(backOffPeriodSupplier, "'backOffPeriodSupplier' cannot be null");
this.maxBackOffPeriod = backOffPeriodSupplier;
}
/**
@@ -98,7 +123,7 @@ public class UniformRandomBackOffPolicy extends StatelessBackOffPolicy
* @return the backoff period
*/
public long getMaxBackOffPeriod() {
return maxBackOffPeriod;
return maxBackOffPeriod.get();
}
/**
@@ -107,9 +132,10 @@ public class UniformRandomBackOffPolicy extends StatelessBackOffPolicy
*/
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);

View File

@@ -26,6 +26,7 @@ import org.springframework.retry.RecoveryCallback;
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 class RetryOperationsInterceptor implements MethodInterceptor {
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

View File

@@ -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<Long> resetTimeoutSupplier;
private Supplier<Long> openTimeoutSupplier;
public CircuitBreakerRetryPolicy() {
this(new SimpleRetryPolicy());
}
@@ -61,6 +66,17 @@ public class CircuitBreakerRetryPolicy implements RetryPolicy {
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<Long> 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 class CircuitBreakerRetryPolicy implements RetryPolicy {
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<Long> timeoutSupplier) {
this.openTimeoutSupplier = timeoutSupplier;
}
@Override
public boolean canRetry(RetryContext context) {
CircuitBreakerRetryContext circuit = (CircuitBreakerRetryContext) context;
@@ -86,7 +113,15 @@ public class CircuitBreakerRetryPolicy implements RetryPolicy {
@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

View File

@@ -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<Integer> maxAttemptsSupplier;
private BinaryExceptionClassifier retryableClassifier = new BinaryExceptionClassifier(false);
@@ -149,11 +153,30 @@ public class SimpleRetryPolicy implements RetryPolicy {
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<Integer> 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 @@ public class SimpleRetryPolicy implements RetryPolicy {
@Override
public String toString() {
return ClassUtils.getShortName(getClass()) + "[maxAttempts=" + this.maxAttempts + "]";
return ClassUtils.getShortName(getClass()) + "[maxAttempts=" + getMaxAttempts() + "]";
}
}

View File

@@ -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;
}
}

View File

@@ -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.context.annotation.Configuration;
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 class CircuitBreakerTests {
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 @@ public class CircuitBreakerTests {
void expressionService2();
void expressionService3();
int getCount();
RetryContext getContext();
@@ -125,9 +161,9 @@ public class CircuitBreakerTests {
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 class CircuitBreakerTests {
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++;
}

View File

@@ -45,8 +45,13 @@ import org.springframework.retry.support.RetryTemplate;
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 class EnableRetryTests {
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 class EnableRetryTests {
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<Object, Map<Method, MethodInterceptor>> delegates = (Map<Object, Map<Method, MethodInterceptor>>) new DirectFieldAccessor(
advice).getPropertyValue("delegates");
MethodInterceptor interceptor = delegates.get(target(service))
.get(ExpressionService.class.getDeclaredMethod("service6"));
RetryTemplate template = (RetryTemplate) new DirectFieldAccessor(interceptor)
.getPropertyValue("retryOperations");
DirectFieldAccessor templateAccessor = new DirectFieldAccessor(template);
ExponentialBackOffPolicy backOff = (ExponentialBackOffPolicy) templateAccessor
.getPropertyValue("backOffPolicy");
assertThat(backOff.getInitialInterval()).isEqualTo(1000);
assertThat(backOff.getMaxInterval()).isEqualTo(2000);
assertThat(backOff.getMultiplier()).isEqualTo(1.2);
SimpleRetryPolicy retryPolicy = (SimpleRetryPolicy) templateAccessor.getPropertyValue("retryPolicy");
assertThat(retryPolicy.getMaxAttempts()).isEqualTo(3);
context.close();
}
private Object target(Object target) {
if (!AopUtils.isAopProxy(target)) {
return target;
@@ -452,6 +494,37 @@ public class EnableRetryTests {
return new RetryableImplementation();
}
@Bean
RuntimeConfigs runtimeConfigs() {
return spy(new RuntimeConfigs());
}
}
public static class RuntimeConfigs {
int count = 0;
public int getMaxAttempts() {
count++;
return 3;
}
public long getInitial() {
count++;
return 1000;
}
public long getMax() {
count++;
return 2000;
}
public double getMult() {
count++;
return 1.2;
}
}
protected static class Service {
@@ -489,6 +562,12 @@ public class EnableRetryTests {
}
}
@Retryable(maxAttemptsExpression = "args[0] == 'foo' ? 3 : 1")
public void conditional(String string) {
this.count++;
throw new IllegalArgumentException("conditional");
}
public int getCount() {
return this.count;
}
@@ -663,6 +742,15 @@ public class EnableRetryTests {
}
}
@Retryable(maxAttemptsExpression = "@runtimeConfigs.maxAttempts",
backoff = @Backoff(delayExpression = "@runtimeConfigs.initial",
maxDelayExpression = "@runtimeConfigs.max", multiplierExpression = "@runtimeConfigs.mult"))
public void service6() {
if (this.count++ < 2) {
throw new RuntimeException("retry");
}
}
public int getCount() {
return this.count;
}

View File

@@ -19,6 +19,7 @@ import java.util.Collections;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import org.aopalliance.intercept.MethodInterceptor;
import org.junit.jupiter.api.Test;
@@ -82,8 +83,9 @@ public class RetryInterceptorBuilderTests {
.backOffPolicy(new FixedBackOffPolicy()).build();
assertThat(TestUtils.getPropertyValue(interceptor, "retryOperations.retryPolicy.maxAttempts")).isEqualTo(5);
assertThat(TestUtils.getPropertyValue(interceptor, "retryOperations.backOffPolicy.backOffPeriod"))
.isEqualTo(1000L);
assertThat(TestUtils
.getPropertyValue(interceptor, "retryOperations.backOffPolicy.backOffPeriod", Supplier.class).get())
.isEqualTo(1000L);
}
@Test
@@ -96,8 +98,9 @@ public class RetryInterceptorBuilderTests {
}).backOffPolicy(new FixedBackOffPolicy()).build();
assertThat(TestUtils.getPropertyValue(interceptor, "retryOperations.retryPolicy.maxAttempts")).isEqualTo(5);
assertThat(TestUtils.getPropertyValue(interceptor, "retryOperations.backOffPolicy.backOffPeriod"))
.isEqualTo(1000L);
assertThat(TestUtils
.getPropertyValue(interceptor, "retryOperations.backOffPolicy.backOffPeriod", Supplier.class).get())
.isEqualTo(1000L);
final AtomicInteger count = new AtomicInteger();
Foo delegate = createDelegate(interceptor, count);
Object message = "";