Skip to content

GH-184: Expression Evaluation at Runtime #298

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it only a limitation for the maxAttempts option?

I see open() is done far before a doWithRetry(), so perhaps those args does not make sense for circuit breaker...

Looks like BackOff expressions don't suffer from this limitation: it is indeed called in the catch from the doWithRetry().

Do I miss anything?

Thanks

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right - the back off is not evaluated until after the method is called so, yes, the limitation is only on the maxAttempts.

Good point about the CB.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out that we can't use args at all for any stateful retry (including circuit breakers) because you would get the args from the previous call (which may be different).

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it expected to propagate an args var as null for statefull unlike for stateless with an Args.NO_ARGS?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes; I want them to get a hard error EL1007E: Property or field 'args' cannot be found on null.

I failed to save the doc change for this; pushing now...

}

/**
Expand Down
Loading