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[] includes = (Class[]) attrs.get("value"); @@ -340,21 +340,25 @@ private RetryPolicy getRetryPolicy(Annotation retryable) { Class[] excludes = (Class[]) 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> delegates = (Map>) 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 NotAnnotatedInterface notAnnotatedInterface() { 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 void other() { } } + @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 void service5() { } } + @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; } diff --git a/src/test/java/org/springframework/retry/interceptor/RetryInterceptorBuilderTests.java b/src/test/java/org/springframework/retry/interceptor/RetryInterceptorBuilderTests.java index 5b98ea78..63a405e1 100644 --- a/src/test/java/org/springframework/retry/interceptor/RetryInterceptorBuilderTests.java +++ b/src/test/java/org/springframework/retry/interceptor/RetryInterceptorBuilderTests.java @@ -19,6 +19,7 @@ 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 void testWithCustomBackOffPolicy() { .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 void testWithCustomNewMessageIdentifier() throws Exception { }).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 = "";