Skip to content

Commit

Permalink
Add expression to @retryable
Browse files Browse the repository at this point in the history
  • Loading branch information
garyrussell committed Dec 16, 2015
1 parent c2c3acb commit f2c98b7
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014 the original author or authors.
* Copyright 2014-2015 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.
Expand Down Expand Up @@ -40,6 +40,7 @@
import org.springframework.retry.interceptor.MethodInvocationRecoverer;
import org.springframework.retry.interceptor.NewMethodArgumentsIdentifier;
import org.springframework.retry.interceptor.RetryInterceptorBuilder;
import org.springframework.retry.policy.ExpressionRetryPolicy;
import org.springframework.retry.policy.MapRetryContextCache;
import org.springframework.retry.policy.RetryContextCache;
import org.springframework.retry.policy.SimpleRetryPolicy;
Expand All @@ -54,6 +55,7 @@
*
* @author Dave Syer
* @author Artem Bilan
* @author Gary Russell
* @since 1.1
*
*/
Expand Down Expand Up @@ -194,13 +196,15 @@ public void doWith(Method method) throws IllegalArgumentException,
}

private RetryPolicy getRetryPolicy(Retryable retryable) {
boolean hasExpression = StringUtils.hasText(retryable.expression());
Class<? extends Throwable>[] includes = retryable.value();
if (includes.length == 0) {
includes = retryable.include();
}
Class<? extends Throwable>[] excludes = retryable.exclude();
if (includes.length == 0 && excludes.length == 0) {
SimpleRetryPolicy simple = new SimpleRetryPolicy();
SimpleRetryPolicy simple = hasExpression ? new ExpressionRetryPolicy(retryable.expression())
: new SimpleRetryPolicy();
simple.setMaxAttempts(retryable.maxAttempts());
return simple;
}
Expand All @@ -211,7 +215,12 @@ private RetryPolicy getRetryPolicy(Retryable retryable) {
for (Class<? extends Throwable> type : excludes) {
policyMap.put(type, false);
}
return new SimpleRetryPolicy(retryable.maxAttempts(), policyMap, true);
if (hasExpression) {
return new ExpressionRetryPolicy(retryable.maxAttempts(), policyMap, true, retryable.expression());
}
else {
return new SimpleRetryPolicy(retryable.maxAttempts(), policyMap, true);
}
}

private BackOffPolicy getBackoffPolicy(Backoff backoff) {
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/org/springframework/retry/annotation/Retryable.java
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,15 @@
*/
Backoff backoff() default @Backoff();

/**
* Specify an expression to be evaluated after the {@code SimpleRetryPolicy.canRetry()}
* returns true - can be used to conditionally suppress the retry. Only invoked after
* an exception is thrown. The root object for the evaluation is the last {@code Throwable}.
* For example:
* <pre class=code>
* {@code "message.contains('you can retry this')"}.
* </pre>
* @return the expression.
*/
String expression() default "";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright 2015 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
*
* http://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.policy;

import java.util.Map;

import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.retry.RetryContext;
import org.springframework.util.Assert;

/**
* @author Gary Russell
* @since 1.2
*
*/
public class ExpressionRetryPolicy extends SimpleRetryPolicy {

private final Expression expression;

/**
* Construct an instance with the provided {@link Expression}.
* @param expression the expression
*/
public ExpressionRetryPolicy(Expression expression) {
super();
Assert.notNull(expression, "'expression' cannot be null");
this.expression = expression;
}

/**
* Construct an instance with the provided expression.
* @param expressionString the expression.
*/
public ExpressionRetryPolicy(String expressionString) {
super();
Assert.notNull(expressionString, "'expressionString' cannot be null");
this.expression = new SpelExpressionParser().parseExpression(expressionString);
}

/**
* Construct an instance with the provided {@link Expression}.
* @param maxAttempts the max attempts
* @param retryableExceptions the exceptions
* @param traverseCauses true to examine causes
* @param expression the expression
*/
public ExpressionRetryPolicy(int maxAttempts, Map<Class<? extends Throwable>, Boolean> retryableExceptions,
boolean traverseCauses, Expression expression) {
super(maxAttempts, retryableExceptions, traverseCauses);
Assert.notNull(expression, "'expression' cannot be null");
this.expression = expression;
}

/**
* Construct an instance with the provided expression.
* @param maxAttempts the max attempts
* @param retryableExceptions the exceptions
* @param traverseCauses true to examine causes
* @param expressionString the expression.
*/
public ExpressionRetryPolicy(int maxAttempts, Map<Class<? extends Throwable>, Boolean> retryableExceptions,
boolean traverseCauses, String expressionString) {
super(maxAttempts, retryableExceptions, traverseCauses);
Assert.notNull(expressionString, "'expressionString' cannot be null");
this.expression = new SpelExpressionParser().parseExpression(expressionString);

This comment has been minimized.

Copy link
@scottmf

scottmf Dec 16, 2015

Not sure how the error handling bubbles up in this mechanism, I would just make sure that any parse errors are properly logged.

This comment has been minimized.

Copy link
@garyrussell

garyrussell Dec 18, 2015

Author Owner

A parser error here would prevent the context from initializing.

}

@Override
public boolean canRetry(RetryContext context) {
Throwable lastThrowable = context.getLastThrowable();
if (lastThrowable == null) {
return super.canRetry(context);
}
else {
return super.canRetry(context) && this.expression.getValue(lastThrowable, Boolean.class);

This comment has been minimized.

Copy link
@scottmf

scottmf Dec 16, 2015

Same here - just make sure that errors are properly logged

This comment has been minimized.

Copy link
@garyrussell

garyrussell Dec 18, 2015

Author Owner

Yeah; as I said, this was a quick hack to prove the concept. I would think if we had a runtime evaluation exception here, we'd log it and return false.

}
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014 the original author or authors.
* Copyright 2014-2015 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.
Expand Down Expand Up @@ -35,6 +35,7 @@
/**
* @author Dave Syer
* @author Artem Bilan
* @author Gary Russell
* @since 1.1
*/
public class EnableRetryTests {
Expand Down Expand Up @@ -145,6 +146,23 @@ public void testExternalInterceptor() {
context.close();
}

@Test
public void testExpression() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class);
ExpressionService service = context.getBean(ExpressionService.class);
service.service1();
assertEquals(3, service.getCount());
try {
service.service2();
fail("expected exception");
}
catch (RuntimeException e) {
assertEquals("this cannot be retried", e.getMessage());
}
assertEquals(4, service.getCount());
context.close();
}

@Configuration
@EnableRetry(proxyTargetClass = true)
protected static class TestProxyConfiguration {
Expand Down Expand Up @@ -211,6 +229,11 @@ public InterceptableService serviceWithExternalInterceptor() {
return new InterceptableService();
}

@Bean
public ExpressionService expressionService() {
return new ExpressionService();
}

@Bean
public Foo foo() {
return new Foo();
Expand Down Expand Up @@ -354,6 +377,29 @@ public int getCount() {

}

private static class ExpressionService {

private int count = 0;

@Retryable(expression="message.contains('this can be retried')")
public void service1() {
if (count++ < 2) {
throw new RuntimeException("this can be retried");
}
}

@Retryable(expression="message.contains('this can be retried')")
public void service2() {
count++;
throw new RuntimeException("this cannot be retried");
}

public int getCount() {
return count;
}

}

private static class Foo {

}
Expand Down

1 comment on commit f2c98b7

@scottmf
Copy link

Choose a reason for hiding this comment

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

looks good, thanks for jumping on this!

Please sign in to comment.