Skip to content

Commit

Permalink
GH-184: Expression Evaluation at Runtime
Browse files Browse the repository at this point in the history
Resolves #184

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

Other polishing from review.

* Docs for stateful.
  • Loading branch information
garyrussell authored May 4, 2022
1 parent 7a86c9e commit 47c1e52
Show file tree
Hide file tree
Showing 17 changed files with 707 additions and 156 deletions.
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -211,8 +215,8 @@ private <A extends Annotation> 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();
}
Expand All @@ -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();
Expand All @@ -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) {
Expand Down Expand Up @@ -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<String, Object> attrs = AnnotationUtils.getAnnotationAttributes(retryable);
@SuppressWarnings("unchecked")
Class<? extends Throwable>[] includes = (Class<? extends Throwable>[]) attrs.get("value");
Expand All @@ -340,21 +340,25 @@ private RetryPolicy getRetryPolicy(Annotation retryable) {
Class<? extends Throwable>[] excludes = (Class<? extends Throwable>[]) attrs.get("exclude");
Integer maxAttempts = (Integer) attrs.get("maxAttempts");
String maxAttemptsExpression = (String) attrs.get("maxAttemptsExpression");
Expression parsedExpression = null;
if (StringUtils.hasText(maxAttemptsExpression)) {
if (ExpressionRetryPolicy.isTemplate(maxAttemptsExpression)) {
maxAttempts = PARSER.parseExpression(resolve(maxAttemptsExpression), PARSER_CONTEXT)
.getValue(this.evaluationContext, Integer.class);
}
else {
maxAttempts = PARSER.parseExpression(resolve(maxAttemptsExpression)).getValue(this.evaluationContext,
Integer.class);
parsedExpression = parse(maxAttemptsExpression);
if (isTemplate(maxAttemptsExpression)) {
maxAttempts = parsedExpression.getValue(this.evaluationContext, Integer.class);
parsedExpression = null;
}
}
final Expression expression = parsedExpression;
if (includes.length == 0 && excludes.length == 0) {
SimpleRetryPolicy simple = hasExpression
? new ExpressionRetryPolicy(resolve(exceptionExpression)).withBeanFactory(this.beanFactory)
: new SimpleRetryPolicy();
simple.setMaxAttempts(maxAttempts);
if (expression != null) {
simple.setMaxAttempts(() -> evaluate(expression, Integer.class, stateless));
}
else {
simple.setMaxAttempts(maxAttempts);
}
return simple;
}
Map<Class<? extends Throwable>, Boolean> policyMap = new HashMap<>();
Expand All @@ -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<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);
}

/**
Expand Down
Loading

0 comments on commit 47c1e52

Please sign in to comment.