diff --git a/pom.xml b/pom.xml index 26cdd02..2171eac 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.springframework.retry spring-retry - 1.0.4.BUILD-SNAPSHOT + 1.1.0.BUILD-SNAPSHOT Spring Retry @@ -21,7 +21,7 @@ jar true - 3.0.5.RELEASE + 4.0.3.RELEASE @@ -123,14 +123,14 @@ - spring-release + repo.spring.io Spring Release Repository - s3://maven.springframework.org/release + https://repo.spring.io/libs-release-local - spring-snapshot + repo.spring.io Spring Snapshot Repository - s3://maven.springframework.org/snapshot + https://repo.spring.io/libs-snapshot-local @@ -138,7 +138,7 @@ junit junit - 4.8.2 + 4.11 test @@ -156,7 +156,7 @@ org.aspectj aspectjweaver - 1.6.6 + 1.7.4 test @@ -185,22 +185,7 @@ - - - com.springsource.repository.bundles.release - http://repository.springsource.com/maven/bundles/release - - - - - - - org.springframework.build.aws - org.springframework.build.aws.maven - 3.0.0.RELEASE - - @@ -233,8 +218,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.5 - 1.5 + 1.7 + 1.7 @@ -250,20 +235,6 @@ - - com.springsource.bundlor - com.springsource.bundlor.maven - 1.0.0.RELEASE - true - - - bundlor - - bundlor - - - - maven-javadoc-plugin diff --git a/src/main/java/org/springframework/retry/backoff/FixedBackOffPolicy.java b/src/main/java/org/springframework/retry/backoff/FixedBackOffPolicy.java index 18e09f7..942b1c6 100644 --- a/src/main/java/org/springframework/retry/backoff/FixedBackOffPolicy.java +++ b/src/main/java/org/springframework/retry/backoff/FixedBackOffPolicy.java @@ -16,18 +16,18 @@ package org.springframework.retry.backoff; - /** - * Implementation of {@link BackOffPolicy} that pauses for a fixed period of - * time before continuing. A pause is implemented using {@link Thread#sleep(long)}. - *

{@link #setBackOffPeriod(long)} is thread-safe and it is safe to call - * {@link #setBackOffPeriod} during execution from multiple threads, however - * this may cause a single retry operation to have pauses of different - * intervals. + * Implementation of {@link BackOffPolicy} that pauses for a fixed period of time before + * continuing. A pause is implemented using {@link Sleeper#sleep(long)}. + *

+ * {@link #setBackOffPeriod(long)} is thread-safe and it is safe to call + * {@link #setBackOffPeriod} during execution from multiple threads, however this may + * cause a single retry operation to have pauses of different intervals. * @author Rob Harrop * @author Dave Syer */ -public class FixedBackOffPolicy extends StatelessBackOffPolicy implements SleepingBackOffPolicy { +public class FixedBackOffPolicy extends StatelessBackOffPolicy implements + SleepingBackOffPolicy { /** * Default back off period - 1000ms. @@ -39,15 +39,14 @@ public class FixedBackOffPolicy extends StatelessBackOffPolicy implements Sleepi */ private volatile long backOffPeriod = DEFAULT_BACK_OFF_PERIOD; - private Sleeper sleeper = new ObjectWaitSleeper(); - public FixedBackOffPolicy withSleeper(Sleeper sleeper) { - FixedBackOffPolicy res = new FixedBackOffPolicy(); - res.setBackOffPeriod(backOffPeriod); - res.setSleeper(sleeper); - return res; - } + public FixedBackOffPolicy withSleeper(Sleeper sleeper) { + FixedBackOffPolicy res = new FixedBackOffPolicy(); + res.setBackOffPeriod(backOffPeriod); + res.setSleeper(sleeper); + return res; + } /** * Public setter for the {@link Sleeper} strategy. @@ -58,8 +57,7 @@ public class FixedBackOffPolicy extends StatelessBackOffPolicy implements Sleepi } /** - * Set the back off period in milliseconds. Cannot be < 1. Default value - * is 1000ms. + * Set the back off period in milliseconds. Cannot be < 1. Default value is 1000ms. */ public void setBackOffPeriod(long backOffPeriod) { this.backOffPeriod = (backOffPeriod > 0 ? backOffPeriod : 1); @@ -80,13 +78,12 @@ public class FixedBackOffPolicy extends StatelessBackOffPolicy implements Sleepi protected void doBackOff() throws BackOffInterruptedException { try { sleeper.sleep(backOffPeriod); - } - catch (InterruptedException e) { + } catch (InterruptedException e) { throw new BackOffInterruptedException("Thread interrupted while sleeping", e); } } - public String toString() { - return "FixedBackOffPolicy[backOffPeriod=" + backOffPeriod + "]"; - } + public String toString() { + return "FixedBackOffPolicy[backOffPeriod=" + backOffPeriod + "]"; + } } diff --git a/src/main/java/org/springframework/retry/backoff/UniformRandomBackOffPolicy.java b/src/main/java/org/springframework/retry/backoff/UniformRandomBackOffPolicy.java new file mode 100644 index 0000000..5005ae5 --- /dev/null +++ b/src/main/java/org/springframework/retry/backoff/UniformRandomBackOffPolicy.java @@ -0,0 +1,116 @@ +/* + * Copyright 2006-2007 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.backoff; + +import java.util.Random; + + +/** + * Implementation of {@link BackOffPolicy} that pauses for a random period of + * time before continuing. A pause is implemented using {@link Sleeper#sleep(long)}. + *

{@link #setMinBackOffPeriod(long)} is thread-safe and it is safe to call + * {@link #setBackOffPeriod} during execution from multiple threads, however + * this may cause a single retry operation to have pauses of different + * intervals. + * @author Rob Harrop + * @author Dave Syer + */ +public class UniformRandomBackOffPolicy extends StatelessBackOffPolicy implements SleepingBackOffPolicy { + + /** + * Default min back off period - 500ms. + */ + private static final long DEFAULT_BACK_OFF_MIN_PERIOD = 500L; + + /** + * Default max back off period - 1500ms. + */ + private static final long DEFAULT_BACK_OFF_MAX_PERIOD = 1500L; + + private volatile long minBackOffPeriod = DEFAULT_BACK_OFF_MIN_PERIOD; + + private volatile long maxBackOffPeriod = DEFAULT_BACK_OFF_MAX_PERIOD; + + private Random random = new Random(System.currentTimeMillis()); + + private Sleeper sleeper = new ObjectWaitSleeper(); + + public UniformRandomBackOffPolicy withSleeper(Sleeper sleeper) { + UniformRandomBackOffPolicy res = new UniformRandomBackOffPolicy(); + res.setMinBackOffPeriod(minBackOffPeriod); + res.setSleeper(sleeper); + return res; + } + + /** + * Public setter for the {@link Sleeper} strategy. + * @param sleeper the sleeper to set defaults to {@link ObjectWaitSleeper}. + */ + public void setSleeper(Sleeper sleeper) { + this.sleeper = sleeper; + } + + /** + * Set the minimum back off period in milliseconds. Cannot be < 1. Default value + * is 500ms. + */ + public void setMinBackOffPeriod(long backOffPeriod) { + this.minBackOffPeriod = (backOffPeriod > 0 ? backOffPeriod : 1); + } + + /** + * The minimum backoff period in milliseconds. + * @return the backoff period + */ + public long getMinBackOffPeriod() { + return minBackOffPeriod; + } + + /** + * Set the maximum back off period in milliseconds. Cannot be < 1. Default value + * is 1500ms. + */ + public void setMaxBackOffPeriod(long backOffPeriod) { + this.maxBackOffPeriod = (backOffPeriod > 0 ? backOffPeriod : 1); + } + + /** + * The maximum backoff period in milliseconds. + * @return the backoff period + */ + public long getMaxBackOffPeriod() { + return maxBackOffPeriod; + } + + /** + * Pause for the {@link #setMinBackOffPeriod(long)}. + * @throws BackOffInterruptedException if interrupted during sleep. + */ + protected void doBackOff() throws BackOffInterruptedException { + try { + long delta = maxBackOffPeriod==minBackOffPeriod ? 0 : random.nextInt((int) (maxBackOffPeriod - minBackOffPeriod)); + sleeper.sleep(minBackOffPeriod + delta ); + } + catch (InterruptedException e) { + throw new BackOffInterruptedException("Thread interrupted while sleeping", e); + } + } + + public String toString() { + return "RandomBackOffPolicy[backOffPeriod=" + minBackOffPeriod + ", " + maxBackOffPeriod + "]"; + } +} diff --git a/src/main/java/org/springframework/retry/config/AnnotationAwareRetryOperationsInterceptor.java b/src/main/java/org/springframework/retry/config/AnnotationAwareRetryOperationsInterceptor.java new file mode 100644 index 0000000..edda8fc --- /dev/null +++ b/src/main/java/org/springframework/retry/config/AnnotationAwareRetryOperationsInterceptor.java @@ -0,0 +1,202 @@ +/* + * Copyright 2012-2013 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.config; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.retry.RetryPolicy; +import org.springframework.retry.backoff.BackOffPolicy; +import org.springframework.retry.backoff.ExponentialBackOffPolicy; +import org.springframework.retry.backoff.ExponentialRandomBackOffPolicy; +import org.springframework.retry.backoff.FixedBackOffPolicy; +import org.springframework.retry.backoff.UniformRandomBackOffPolicy; +import org.springframework.retry.backoff.Sleeper; +import org.springframework.retry.interceptor.MethodArgumentsKeyGenerator; +import org.springframework.retry.interceptor.NewMethodArgumentsIdentifier; +import org.springframework.retry.interceptor.RetryOperationsInterceptor; +import org.springframework.retry.interceptor.StatefulRetryOperationsInterceptor; +import org.springframework.retry.policy.MapRetryContextCache; +import org.springframework.retry.policy.RetryContextCache; +import org.springframework.retry.policy.SimpleRetryPolicy; +import org.springframework.retry.support.RetryTemplate; + +/** + * Wrapper interceptor that interprets the retry metadata on the method it is invoking and + * delegates to an appropriate RetryOperationsInterceptor. + * + * @author Dave Syer + * + */ +public class AnnotationAwareRetryOperationsInterceptor implements MethodInterceptor { + + private Map delegates = new HashMap(); + + private RetryContextCache retryContextCache = new MapRetryContextCache(); + + private MethodArgumentsKeyGenerator methodArgumentsKeyGenerator; + + private NewMethodArgumentsIdentifier newMethodArgumentsIdentifier; + + private Sleeper sleeper; + + /** + * @param sleeper the sleeper to set + */ + public void setSleeper(Sleeper sleeper) { + this.sleeper = sleeper; + } + + /** + * Public setter for the {@link RetryContextCache}. + * + * @param retryContextCache the {@link RetryContextCache} to set. + */ + public void setRetryContextCache(RetryContextCache retryContextCache) { + this.retryContextCache = retryContextCache; + } + + /** + * @param methodArgumentsKeyGenerator + */ + public void setKeyGenerator(MethodArgumentsKeyGenerator methodArgumentsKeyGenerator) { + this.methodArgumentsKeyGenerator = methodArgumentsKeyGenerator; + } + + /** + * @param newMethodArgumentsIdentifier + */ + public void setNewItemIdentifier( + NewMethodArgumentsIdentifier newMethodArgumentsIdentifier) { + this.newMethodArgumentsIdentifier = newMethodArgumentsIdentifier; + } + + public Object invoke(MethodInvocation invocation) throws Throwable { + MethodInterceptor delegate = getDelegate(invocation.getMethod()); + return delegate.invoke(invocation); + } + + private MethodInterceptor getDelegate(Method method) { + if (!delegates.containsKey(method)) { + synchronized (delegates) { + if (!delegates.containsKey(method)) { + Retryable retryable = AnnotationUtils.findAnnotation(method, + Retryable.class); + if (retryable == null) { + retryable = AnnotationUtils.findAnnotation( + method.getDeclaringClass(), Retryable.class); + } + MethodInterceptor delegate; + if (retryable.stateful()) { + delegate = getStatefulInterceptor(retryable); + } else { + delegate = getStatelessInterceptor(retryable); + } + // TODO: allow declaring class to specify @Recover methods + delegates.put(method, delegate); + } + } + } + return delegates.get(method); + } + + private MethodInterceptor getStatelessInterceptor(Retryable retryable) { + RetryOperationsInterceptor interceptor = new RetryOperationsInterceptor(); + RetryTemplate template = new RetryTemplate(); + template.setRetryPolicy(getRetryPolicy(retryable)); + template.setBackOffPolicy(getBackoffPolicy(retryable.backoff())); + interceptor.setRetryOperations(template); + return interceptor; + } + + private MethodInterceptor getStatefulInterceptor(Retryable retryable) { + StatefulRetryOperationsInterceptor interceptor = new StatefulRetryOperationsInterceptor(); + if (methodArgumentsKeyGenerator != null) { + interceptor.setKeyGenerator(methodArgumentsKeyGenerator); + } + if (newMethodArgumentsIdentifier != null) { + interceptor.setNewItemIdentifier(newMethodArgumentsIdentifier); + } + RetryTemplate template = new RetryTemplate(); + template.setRetryContextCache(retryContextCache); + template.setRetryPolicy(getRetryPolicy(retryable)); + template.setBackOffPolicy(getBackoffPolicy(retryable.backoff())); + interceptor.setRetryOperations(template); + return interceptor; + } + + private RetryPolicy getRetryPolicy(Retryable retryable) { + Class[] includes = retryable.value(); + if (includes.length == 0) { + includes = retryable.include(); + } + Class[] excludes = retryable.exclude(); + if (includes.length == 0 && excludes.length == 0) { + SimpleRetryPolicy simple = new SimpleRetryPolicy(); + simple.setMaxAttempts(retryable.maxAttempts()); + return simple; + } + Map, Boolean> policyMap = new HashMap, Boolean>(); + for (Class type : includes) { + policyMap.put(type, true); + } + for (Class type : excludes) { + policyMap.put(type, false); + } + SimpleRetryPolicy simple = new SimpleRetryPolicy(retryable.maxAttempts(), + policyMap, true); + return simple; + } + + private BackOffPolicy getBackoffPolicy(Backoff backoff) { + long min = backoff.delay()==0 ? backoff.value() : backoff.delay(); + long max = backoff.maxDelay(); + if (backoff.multiplier()>0) { + ExponentialBackOffPolicy policy = new ExponentialBackOffPolicy(); + if (backoff.random()) { + policy = new ExponentialRandomBackOffPolicy(); + } + policy.setInitialInterval(min); + policy.setMultiplier(backoff.multiplier()); + policy.setMaxInterval(max>min ? max : ExponentialBackOffPolicy.DEFAULT_MAX_INTERVAL); + if (sleeper!=null) { + policy.setSleeper(sleeper); + } + return policy; + } + if (max>min) { + UniformRandomBackOffPolicy policy = new UniformRandomBackOffPolicy(); + policy.setMinBackOffPeriod(min); + policy.setMaxBackOffPeriod(max); + if (sleeper!=null) { + policy.setSleeper(sleeper); + } + return policy; + } + FixedBackOffPolicy policy = new FixedBackOffPolicy(); + policy.setBackOffPeriod(min); + if (sleeper!=null) { + policy.setSleeper(sleeper); + } + return policy; + } + +} diff --git a/src/main/java/org/springframework/retry/config/Backoff.java b/src/main/java/org/springframework/retry/config/Backoff.java new file mode 100644 index 0000000..073d48f --- /dev/null +++ b/src/main/java/org/springframework/retry/config/Backoff.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-2013 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.config; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Import; +import org.springframework.retry.backoff.BackOffPolicy; + +/** + * Collects metadata for a {@link BackOffPolicy}. Features: + * + *

    + *
  • With no explicit settings the default is a fixed delay of 1000ms
  • + *
  • Only the {@link #delay()} set: the backoff is a fixed delay with that value
  • + *
  • When {@link #delay()} and {@link #maxDelay()} are set the backoff is uniformly + * distributed between the two values
  • + *
  • With {@link #delay()}, {@link #maxDelay()} and {@link #multiplier()} the backoff is + * exponentially growing up to the maximum value
  • + *
  • If, in addition, the {@link #random()} flag is set then the multiplier is chosen + * for each delay from a uniform distribution in [1, multiplier-1]
  • + *
+ * + * @author Dave Syer + * + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Import(RetryConfiguration.class) +@Documented +public @interface Backoff { + + /** + * Synonym for {@link #delay()}. + * + * @return the delay in milliseconds (default 1000) + */ + long value() default 1000; + + /** + * A canonical backoff period. Used as an initial value in the exponential case, and + * as a minimum value in the uniform case. + * @return the initial or canonical backoff period in milliseconds (default 1000) + */ + long delay() default 0; + + /** + * The maximimum wait (in milliseconds) between retries. If less than the + * {@link #delay()} then ignored. + * + * @return the maximum delay between retries (default 0 = ignored) + */ + long maxDelay() default 0; + + /** + * If positive, then used as a multiplier for generating the next delay for backoff. + * + * @return a multiplier to use to calculate the next backoff delay (default 0 = + * ignored) + */ + double multiplier() default 0; + + /** + * 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. + * + * @return the flag to signal randomization is required (default false) + */ + boolean random() default false; + +} diff --git a/src/main/java/org/springframework/retry/config/EnableRetry.java b/src/main/java/org/springframework/retry/config/EnableRetry.java new file mode 100644 index 0000000..99f1e91 --- /dev/null +++ b/src/main/java/org/springframework/retry/config/EnableRetry.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2013 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.config; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Import; + +/** + * Global enabler for @Retryable annotations in Spring beans. If this is + * declared on any @Configuration in the context then beans that have + * retryable methods will be proxied and the retry handled according to the metadata in + * the annotations. + * + * @author Dave Syer + * + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Import(RetryConfiguration.class) +@Documented +public @interface EnableRetry { + +} diff --git a/src/main/java/org/springframework/retry/config/RetryConfiguration.java b/src/main/java/org/springframework/retry/config/RetryConfiguration.java new file mode 100644 index 0000000..6f3502b --- /dev/null +++ b/src/main/java/org/springframework/retry/config/RetryConfiguration.java @@ -0,0 +1,135 @@ +/* + * Copyright 2012-2013 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.config; + +import java.lang.annotation.Annotation; +import java.util.LinkedHashSet; +import java.util.Set; + +import javax.annotation.PostConstruct; + +import org.aopalliance.aop.Advice; +import org.springframework.aop.Pointcut; +import org.springframework.aop.support.AbstractPointcutAdvisor; +import org.springframework.aop.support.ComposablePointcut; +import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.retry.backoff.Sleeper; +import org.springframework.retry.interceptor.MethodArgumentsKeyGenerator; +import org.springframework.retry.interceptor.NewMethodArgumentsIdentifier; +import org.springframework.retry.policy.RetryContextCache; + +/** + * Basic configuration for @Retryable processing. For stateful retry, if + * there is a unique bean elsewhere in the context of type {@link RetryContextCache}, + * {@link MethodArgumentsKeyGenerator} or {@link NewMethodArgumentsIdentifier} it will be + * used by the corresponding retry interceptor (otherwise sensible defaults are adopted). + * + * @author Dave Syer + * + */ +@SuppressWarnings("serial") +@Configuration +public class RetryConfiguration extends AbstractPointcutAdvisor implements + BeanFactoryAware { + + private Advice advice; + + private Pointcut pointcut; + + @Autowired(required = false) + private RetryContextCache retryContextCache; + + @Autowired(required = false) + private MethodArgumentsKeyGenerator methodArgumentsKeyGenerator; + + @Autowired(required = false) + private NewMethodArgumentsIdentifier newMethodArgumentsIdentifier; + + @Autowired(required = false) + private Sleeper sleeper; + + @PostConstruct + public void init() { + Set> retryableAnnotationTypes = new LinkedHashSet>( + 1); + retryableAnnotationTypes.add(Retryable.class); + this.pointcut = buildPointcut(retryableAnnotationTypes); + this.advice = buildAdvice(); + } + + /** + * Set the {@code BeanFactory} to be used when looking up executors by qualifier. + */ + @Override + public void setBeanFactory(BeanFactory beanFactory) { + if (this.advice instanceof BeanFactoryAware) { + ((BeanFactoryAware) this.advice).setBeanFactory(beanFactory); + } + } + + @Override + public Advice getAdvice() { + return this.advice; + } + + @Override + public Pointcut getPointcut() { + return this.pointcut; + } + + protected Advice buildAdvice() { + AnnotationAwareRetryOperationsInterceptor interceptor = new AnnotationAwareRetryOperationsInterceptor(); + if (retryContextCache != null) { + interceptor.setRetryContextCache(retryContextCache); + } + if (methodArgumentsKeyGenerator != null) { + interceptor.setKeyGenerator(methodArgumentsKeyGenerator); + } + if (newMethodArgumentsIdentifier != null) { + interceptor.setNewItemIdentifier(newMethodArgumentsIdentifier); + } + if (sleeper != null) { + interceptor.setSleeper(sleeper); + } + return interceptor; + } + + /** + * Calculate a pointcut for the given retry annotation types, if any. + * @param retryAnnotationTypes the retry annotation types to introspect + * @return the applicable Pointcut object, or {@code null} if none + */ + protected Pointcut buildPointcut(Set> retryAnnotationTypes) { + ComposablePointcut result = null; + for (Class retryAnnotationType : retryAnnotationTypes) { + Pointcut cpc = new AnnotationMatchingPointcut(retryAnnotationType, true); + Pointcut mpc = AnnotationMatchingPointcut + .forMethodAnnotation(retryAnnotationType); + if (result == null) { + result = new ComposablePointcut(cpc).union(mpc); + } else { + result.union(cpc).union(mpc); + } + } + return result; + } + +} diff --git a/src/main/java/org/springframework/retry/config/Retryable.java b/src/main/java/org/springframework/retry/config/Retryable.java new file mode 100644 index 0000000..cc3b8dc --- /dev/null +++ b/src/main/java/org/springframework/retry/config/Retryable.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-2013 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.config; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Import; + +/** + * Annotation for a method invocation that is retryable. + * + * @author Dave Syer + * + */ +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Import(RetryConfiguration.class) +@Documented +public @interface Retryable { + + /** + * Exception types that are retryable. Synonym for includes(). Defaults to empty (and + * if excludes is also empty all exceptions are retried). + * + * @return exception types to retry + */ + Class[] value() default {}; + + /** + * Exception types that are retryable. Defaults to empty (and if excludes is also + * empty all exceptions are retried). + * + * @return exception types to retry + */ + Class[] include() default {}; + + /** + * Exception types that are not retryable. Defaults to empty (and if includes is also + * empty all exceptions are retried). + * + * @return exception types to retry + */ + Class[] exclude() default {}; + + /** + * Flag to say that the retry is stateful: i.e. exceptions are re-thrown, but the + * retry policy is applied with the same policy to subsequent invocations with the + * same arguments. If false then retryable exceptions are not re-thrown. + * + * @return true if retry is stateful, default false + */ + boolean stateful() default false; + + /** + * @return the maximum number of attempts (including the first failure), defaults to 3 + */ + int maxAttempts() default 3; + + Backoff backoff() default @Backoff(); + +} diff --git a/src/main/java/org/springframework/retry/policy/ExceptionClassifierRetryPolicy.java b/src/main/java/org/springframework/retry/policy/ExceptionClassifierRetryPolicy.java index c7ef498..bc76774 100644 --- a/src/main/java/org/springframework/retry/policy/ExceptionClassifierRetryPolicy.java +++ b/src/main/java/org/springframework/retry/policy/ExceptionClassifierRetryPolicy.java @@ -84,7 +84,7 @@ public class ExceptionClassifierRetryPolicy implements RetryPolicy { } /** - * Create an active context that proxies a retry policy by chosing a target + * Create an active context that proxies a retry policy by choosing a target * from the policy map. * * @see org.springframework.retry.RetryPolicy#open(RetryContext) diff --git a/src/main/java/org/springframework/retry/support/RetrySimulator.java b/src/main/java/org/springframework/retry/support/RetrySimulator.java index e848e34..3a5fb9d 100644 --- a/src/main/java/org/springframework/retry/support/RetrySimulator.java +++ b/src/main/java/org/springframework/retry/support/RetrySimulator.java @@ -102,7 +102,8 @@ public class RetrySimulator { } } - static class FailingRetryException extends Exception { + @SuppressWarnings("serial") + static class FailingRetryException extends Exception { } static class StealingSleeper implements Sleeper { diff --git a/src/test/java/org/springframework/retry/AnyThrowTests.java b/src/test/java/org/springframework/retry/AnyThrowTests.java new file mode 100644 index 0000000..db5245c --- /dev/null +++ b/src/test/java/org/springframework/retry/AnyThrowTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2013 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; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +/** + * @author Dave Syer + * + */ +public class AnyThrowTests { + + @Rule + public ExpectedException expected = ExpectedException.none(); + + @Test + public void testRuntimeException() throws Throwable { + expected.expect(RuntimeException.class); + AnyThrow.throwAny(new RuntimeException("planned")); + } + + @Test + public void testUncheckedRuntimeException() throws Throwable { + expected.expect(RuntimeException.class); + AnyThrow.throwUnchecked(new RuntimeException("planned")); + } + + @Test + public void testCheckedException() throws Throwable { + expected.expect(Exception.class); + AnyThrow.throwAny(new Exception("planned")); + } + + private static class AnyThrow { + + private static void throwUnchecked(Throwable e) { + AnyThrow.throwAny(e); + } + + @SuppressWarnings("unchecked") + private static void throwAny(Throwable e) throws E { + throw (E)e; + } + } + +} diff --git a/src/test/java/org/springframework/retry/config/EnableRetryTests.java b/src/test/java/org/springframework/retry/config/EnableRetryTests.java new file mode 100644 index 0000000..c69bb13 --- /dev/null +++ b/src/test/java/org/springframework/retry/config/EnableRetryTests.java @@ -0,0 +1,180 @@ +/* + * Copyright 2012-2013 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.config; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import org.junit.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.retry.config.EnableRetry; +import org.springframework.retry.config.Retryable; + +/** + * @author Dave Syer + * + */ +public class EnableRetryTests { + + @Test + public void vanilla() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + TestConfiguration.class); + Service service = context.getBean(Service.class); + service.service(); + assertEquals(3, service.getCount()); + context.close(); + } + + @Test + public void type() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + TestConfiguration.class); + RetryableService service = context.getBean(RetryableService.class); + service.service(); + assertEquals(3, service.getCount()); + context.close(); + } + + @Test + public void excludes() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + TestConfiguration.class); + ExcludesService service = context.getBean(ExcludesService.class); + try { + service.service(); + fail("Expected IllegalStateException"); + } catch (IllegalStateException e) { + } + assertEquals(1, service.getCount()); + context.close(); + } + + @Test + public void stateful() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + TestConfiguration.class); + StatefulService service = context.getBean(StatefulService.class); + for (int i = 0; i < 3; i++) { + try { + service.service(1); + } catch (Exception e) { + assertEquals("Planned", e.getMessage()); + } + } + assertEquals(3, service.getCount()); + context.close(); + } + + @Configuration + @EnableRetry + @EnableAspectJAutoProxy(proxyTargetClass = true) + protected static class TestConfiguration { + + @Bean + public Service service() { + return new Service(); + } + + @Bean + public RetryableService retryable() { + return new RetryableService(); + } + + @Bean + public StatefulService stateful() { + return new StatefulService(); + } + + @Bean + public ExcludesService excludes() { + return new ExcludesService(); + } + + } + + protected static class Service { + + private int count = 0; + + @Retryable(RuntimeException.class) + public void service() { + if (count++ < 2) { + throw new RuntimeException("Planned"); + } + } + + public int getCount() { + return count; + } + + } + + @Retryable(RuntimeException.class) + protected static class RetryableService { + + private int count = 0; + + public void service() { + if (count++ < 2) { + throw new RuntimeException("Planned"); + } + } + + public int getCount() { + return count; + } + + } + + protected static class ExcludesService { + + private int count = 0; + + @Retryable(include = RuntimeException.class, exclude = IllegalStateException.class) + public void service() { + if (count++ < 2) { + throw new IllegalStateException("Planned"); + } + } + + public int getCount() { + return count; + } + + } + + protected static class StatefulService { + + private int count = 0; + + @Retryable(stateful = true) + public void service(int value) { + if (count++ < 2) { + throw new RuntimeException("Planned"); + } + } + + public int getCount() { + return count; + } + + } +} diff --git a/src/test/java/org/springframework/retry/config/EnableRetryWithBackoffTests.java b/src/test/java/org/springframework/retry/config/EnableRetryWithBackoffTests.java new file mode 100644 index 0000000..88c889c --- /dev/null +++ b/src/test/java/org/springframework/retry/config/EnableRetryWithBackoffTests.java @@ -0,0 +1,194 @@ +/* + * Copyright 2012-2013 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.config; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.retry.backoff.Sleeper; + +/** + * @author Dave Syer + * + */ +public class EnableRetryWithBackoffTests { + + @Test + public void vanilla() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + TestConfiguration.class); + Service service = context.getBean(Service.class); + service.service(); + assertEquals("[1000, 1000]", context.getBean(TestConfiguration.class).periods.toString()); + assertEquals(3, service.getCount()); + context.close(); + } + + @Test + public void type() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + TestConfiguration.class); + RandomService service = context.getBean(RandomService.class); + service.service(); + List periods = context.getBean(TestConfiguration.class).periods; + assertTrue("Wrong periods: " + periods, periods.get(0)>1000); + assertEquals(3, service.getCount()); + context.close(); + } + + @Test + public void exponential() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + TestConfiguration.class); + ExponentialService service = context.getBean(ExponentialService.class); + service.service(); + assertEquals(3, service.getCount()); + assertEquals("[1000, 1100]", context.getBean(TestConfiguration.class).periods.toString()); + context.close(); + } + + @Test + public void randomExponential() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + TestConfiguration.class); + ExponentialRandomService service = context + .getBean(ExponentialRandomService.class); + service.service(1); + assertEquals(3, service.getCount()); + List periods = context.getBean(TestConfiguration.class).periods; + assertNotEquals("[1000, 1100]", context.getBean(TestConfiguration.class).periods.toString()); + assertTrue("Wrong periods: " + periods, periods.get(0)>1000); + assertTrue("Wrong periods: " + periods, periods.get(1)>1100 && periods.get(1)<1210); + context.close(); + } + + @Configuration + @EnableRetry + @EnableAspectJAutoProxy(proxyTargetClass = true) + protected static class TestConfiguration { + + private List periods = new ArrayList(); + + @Bean + public Sleeper sleper() { + return new Sleeper() { + @Override + public void sleep(long period) throws InterruptedException { + periods.add(period); + } + }; + } + + @Bean + public Service service() { + return new Service(); + } + + @Bean + public RandomService retryable() { + return new RandomService(); + } + + @Bean + public ExponentialRandomService stateful() { + return new ExponentialRandomService(); + } + + @Bean + public ExponentialService excludes() { + return new ExponentialService(); + } + + } + + protected static class Service { + + private int count = 0; + + @Retryable(backoff = @Backoff(delay = 1000)) + public void service() { + if (count++ < 2) { + throw new RuntimeException("Planned"); + } + } + + public int getCount() { + return count; + } + + } + + @Retryable(backoff = @Backoff(delay = 1000, maxDelay = 2000)) + protected static class RandomService { + + private int count = 0; + + public void service() { + if (count++ < 2) { + throw new RuntimeException("Planned"); + } + } + + public int getCount() { + return count; + } + + } + + protected static class ExponentialService { + + private int count = 0; + + @Retryable(backoff = @Backoff(delay = 1000, maxDelay = 2000, multiplier = 1.1)) + public void service() { + if (count++ < 2) { + throw new IllegalStateException("Planned"); + } + } + + public int getCount() { + return count; + } + + } + + protected static class ExponentialRandomService { + + private int count = 0; + + @Retryable(backoff = @Backoff(delay = 1000, maxDelay = 2000, multiplier = 1.1, random=true)) + public void service(int value) { + if (count++ < 2) { + throw new RuntimeException("Planned"); + } + } + + public int getCount() { + return count; + } + + } +} diff --git a/src/test/java/org/springframework/retry/interceptor/RetryOperationsInterceptorTests.java b/src/test/java/org/springframework/retry/interceptor/RetryOperationsInterceptorTests.java index 1504108..be5088c 100644 --- a/src/test/java/org/springframework/retry/interceptor/RetryOperationsInterceptorTests.java +++ b/src/test/java/org/springframework/retry/interceptor/RetryOperationsInterceptorTests.java @@ -133,6 +133,7 @@ public class RetryOperationsInterceptorTests { assertEquals(2, count); // Expect 2 separate transactions... assertEquals(2, transactionCount); + context.close(); } @Test