-
Notifications
You must be signed in to change notification settings - Fork 532
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
Changes from all commits
b6a7632
5ded394
79e93d9
7077a70
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 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(); | ||
} | ||
|
@@ -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<String, Object> attrs = AnnotationUtils.getAnnotationAttributes(retryable); | ||
@SuppressWarnings("unchecked") | ||
Class<? extends Throwable>[] includes = (Class<? extends Throwable>[]) attrs.get("value"); | ||
|
@@ -340,21 +340,25 @@ private RetryPolicy getRetryPolicy(Annotation retryable) { | |
Class<? extends Throwable>[] excludes = (Class<? extends Throwable>[]) attrs.get("exclude"); | ||
Integer maxAttempts = (Integer) attrs.get("maxAttempts"); | ||
String maxAttemptsExpression = (String) attrs.get("maxAttemptsExpression"); | ||
Expression parsedExpression = null; | ||
if (StringUtils.hasText(maxAttemptsExpression)) { | ||
if (ExpressionRetryPolicy.isTemplate(maxAttemptsExpression)) { | ||
maxAttempts = PARSER.parseExpression(resolve(maxAttemptsExpression), PARSER_CONTEXT) | ||
.getValue(this.evaluationContext, Integer.class); | ||
} | ||
else { | ||
maxAttempts = PARSER.parseExpression(resolve(maxAttemptsExpression)).getValue(this.evaluationContext, | ||
Integer.class); | ||
parsedExpression = parse(maxAttemptsExpression); | ||
if (isTemplate(maxAttemptsExpression)) { | ||
maxAttempts = parsedExpression.getValue(this.evaluationContext, Integer.class); | ||
parsedExpression = null; | ||
} | ||
} | ||
final Expression expression = parsedExpression; | ||
if (includes.length == 0 && excludes.length == 0) { | ||
SimpleRetryPolicy simple = hasExpression | ||
? new ExpressionRetryPolicy(resolve(exceptionExpression)).withBeanFactory(this.beanFactory) | ||
: new SimpleRetryPolicy(); | ||
simple.setMaxAttempts(maxAttempts); | ||
if (expression != null) { | ||
simple.setMaxAttempts(() -> evaluate(expression, Integer.class, stateless)); | ||
} | ||
else { | ||
simple.setMaxAttempts(maxAttempts); | ||
} | ||
return simple; | ||
} | ||
Map<Class<? extends Throwable>, 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<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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it expected to propagate an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes; I want them to get a hard error I failed to save the doc change for this; pushing now... |
||
} | ||
|
||
/** | ||
|
There was a problem hiding this comment.
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 adoWithRetry()
, so perhaps thoseargs
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 thedoWithRetry()
.Do I miss anything?
Thanks
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).