commit 2215dddc699b340df760a85a3689fd7bcf304715 Author: Dave Syer Date: Tue Dec 21 17:03:23 2010 +0000 Initial port from Spring Batch - no tests diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c73cd9a --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +target +.springBeans +.settings +.classpath +.project + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..853f188 --- /dev/null +++ b/pom.xml @@ -0,0 +1,156 @@ + + + 4.0.0 + org.springframework + spring-retry + 1.0.0.BUILD-SNAPSHOT + Spring Retry + http://www.springsource.org + jar + + true + 3.0.5.RELEASE + + + + strict + + false + + + + fast + + true + true + + + + staging + + + spring-site-staging + file:///${java.io.tmpdir}/spring-amqp/docs + + + spring-milestone-staging + file:///${java.io.tmpdir}/spring-amqp/milestone + + + spring-snapshot-staging + file:///${java.io.tmpdir}/spring-amqp/snapshot + + + + + + + + http://www.springsource.com/download/community + + + spring-docs + scp://static.springframework.org/var/www/domains/springframework.org/static/htdocs/spring-retry/docs/${project.version} + + + + spring-milestone + Spring Milestone Repository + s3://maven.springframework.org/milestone + + + spring-snapshot + Spring Snapshot Repository + s3://maven.springframework.org/snapshot + + + + + + junit + junit + 4.8.2 + test + + + org.springframework + spring-test + ${spring.framework.version} + test + + + org.springframework + spring-context + ${spring.framework.version} + + + log4j + log4j + 1.2.14 + + + + + + + + maven-assembly-plugin + false + + + project + + + + + org.apache.maven.plugins + maven-jar-plugin + + + target/classes/META-INF/MANIFEST.MF + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.5 + 1.5 + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + **/*Tests.java + + + **/Abstract*.java + + + + + com.springsource.bundlor + com.springsource.bundlor.maven + 1.0.0.RELEASE + true + + + bundlor + + bundlor + + + + + + + diff --git a/src/main/java/org/springframework/commons/classify/BinaryExceptionClassifier.java b/src/main/java/org/springframework/commons/classify/BinaryExceptionClassifier.java new file mode 100644 index 0000000..ebfbfa4 --- /dev/null +++ b/src/main/java/org/springframework/commons/classify/BinaryExceptionClassifier.java @@ -0,0 +1,91 @@ +/* + * 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.commons.classify; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * A {@link Classifier} for exceptions that has only two classes (true and + * false). Classifies objects according to their inheritance relation with the + * supplied types. If the object to be classified is one of the provided types, + * or is a subclass of one of the types, then the non-default value is returned + * (usually true). + * + * @see SubclassClassifier + * + * @author Dave Syer + * + */ +public class BinaryExceptionClassifier extends SubclassClassifier { + + /** + * Create a binary exception classifier with the provided default value. + * + * @param defaultValue defaults to false + */ + public BinaryExceptionClassifier(boolean defaultValue) { + super(defaultValue); + } + + /** + * Create a binary exception classifier with the provided classes and their + * subclasses. The mapped value for these exceptions will be the one + * provided (which will be the opposite of the default). + * + * @param value + */ + public BinaryExceptionClassifier(Collection> exceptionClasses, boolean value) { + this(!value); + if (exceptionClasses != null) { + Map, Boolean> map = new HashMap, Boolean>(); + for (Class type : exceptionClasses) { + map.put(type, !getDefault()); + } + setTypeMap(map); + } + } + + /** + * Create a binary exception classifier with the default value false and + * value mapping true for the provided classes and their subclasses. + */ + public BinaryExceptionClassifier(Collection> exceptionClasses) { + this(exceptionClasses, true); + } + + /** + * Create a binary exception classifier using the given classification map + * and a default classification of false. + * + * @param typeMap + */ + public BinaryExceptionClassifier(Map, Boolean> typeMap) { + this(typeMap, false); + } + + /** + * Create a binary exception classifier using the given classification map + * and a default classification of false. + * + * @param typeMap + */ + public BinaryExceptionClassifier(Map, Boolean> typeMap, boolean defaultValue) { + super(typeMap, defaultValue); + } + +} diff --git a/src/main/java/org/springframework/commons/classify/Classifier.java b/src/main/java/org/springframework/commons/classify/Classifier.java new file mode 100644 index 0000000..83864ac --- /dev/null +++ b/src/main/java/org/springframework/commons/classify/Classifier.java @@ -0,0 +1,38 @@ +/* + * 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.commons.classify; + +/** + * Interface for a classifier. At its simplest a {@link Classifier} is just a + * map from objects of one type to objects of another type. + * + * @author Dave Syer + * + */ +public interface Classifier { + + /** + * Classify the given object and return an object of a different type, + * possibly an enumerated type. + * + * @param classifiable the input object. Can be null. + * @return an object. Can be null, but implementations should declare if + * this is the case. + */ + T classify(C classifiable); + +} diff --git a/src/main/java/org/springframework/commons/classify/ClassifierSupport.java b/src/main/java/org/springframework/commons/classify/ClassifierSupport.java new file mode 100644 index 0000000..c76877c --- /dev/null +++ b/src/main/java/org/springframework/commons/classify/ClassifierSupport.java @@ -0,0 +1,48 @@ +/* + * 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.commons.classify; + +/** + * Base class for {@link Classifier} implementations. Provides default behaviour + * and some convenience members, like constants. + * + * @author Dave Syer + * + */ +public class ClassifierSupport implements Classifier { + + final private T defaultValue; + + /** + * @param defaultValue + */ + public ClassifierSupport(T defaultValue) { + super(); + this.defaultValue = defaultValue; + } + + /** + * Always returns the default value. This is the main extension point for + * subclasses, so it must be able to classify null. + * + * @see org.springframework.commons.classify.Classifier#classify(Object) + */ + public T classify(C throwable) { + return defaultValue; + } + +} diff --git a/src/main/java/org/springframework/commons/classify/SubclassClassifier.java b/src/main/java/org/springframework/commons/classify/SubclassClassifier.java new file mode 100644 index 0000000..f23deb6 --- /dev/null +++ b/src/main/java/org/springframework/commons/classify/SubclassClassifier.java @@ -0,0 +1,148 @@ +/* + * 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.commons.classify; + +import java.io.Serializable; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +/** + * A {@link Classifier} for a parameterised object type based on a map. + * Classifies objects according to their inheritance relation with the supplied + * type map. If the object to be classified is one of the keys of the provided + * map, or is a subclass of one of the keys, then the map entry vale for that + * key is returned. Otherwise returns the default value which is null by + * default. + * + * @author Dave Syer + * + */ +public class SubclassClassifier implements Classifier { + + private Map, C> classified = new HashMap, C>(); + + private C defaultValue = null; + + /** + * Create a {@link SubclassClassifier} with null default value. + * + */ + public SubclassClassifier() { + this(null); + } + + /** + * Create a {@link SubclassClassifier} with supplied default value. + * + * @param defaultValue + */ + public SubclassClassifier(C defaultValue) { + this(new HashMap, C>(), defaultValue); + } + + /** + * Create a {@link SubclassClassifier} with supplied default value. + * + * @param defaultValue + */ + public SubclassClassifier(Map, C> typeMap, C defaultValue) { + super(); + this.classified = new HashMap, C>(typeMap); + this.defaultValue = defaultValue; + } + + /** + * Public setter for the default value for mapping keys that are not found + * in the map (or their subclasses). Defaults to false. + * + * @param defaultValue the default value to set + */ + public void setDefaultValue(C defaultValue) { + this.defaultValue = defaultValue; + } + + /** + * Set the classifications up as a map. The keys are types and these will be + * mapped along with all their subclasses to the corresponding value. The + * most specific types will match first. + * + * @param map a map from type to class + */ + public void setTypeMap(Map, C> map) { + this.classified = new HashMap, C>(map); + } + + /** + * Return the value from the type map whose key is the class of the given + * Throwable, or its nearest ancestor if a subclass. + * + */ + public C classify(T classifiable) { + + if (classifiable == null) { + return defaultValue; + } + + @SuppressWarnings("unchecked") + Class exceptionClass = (Class) classifiable.getClass(); + if (classified.containsKey(exceptionClass)) { + return classified.get(exceptionClass); + } + + // check for subclasses + Set> classes = new TreeSet>(new ClassComparator()); + classes.addAll(classified.keySet()); + for (Class cls : classes) { + if (cls.isAssignableFrom(exceptionClass)) { + C value = classified.get(cls); + this.classified.put(exceptionClass, value); + return value; + } + } + + return defaultValue; + } + + /** + * Return the default value supplied in the constructor (default false). + */ + final public C getDefault() { + return defaultValue; + } + + /** + * Comparator for classes to order by inheritance. + * + * @author Dave Syer + * + */ + private static class ClassComparator implements Comparator>, Serializable { + /** + * @return 1 if arg0 is assignable from arg1, -1 otherwise + * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object) + */ + public int compare(Class arg0, Class arg1) { + if (arg0.isAssignableFrom(arg1)) { + return 1; + } + return -1; + } + } + +} diff --git a/src/main/java/org/springframework/commons/repeat/RepeatCallback.java b/src/main/java/org/springframework/commons/repeat/RepeatCallback.java new file mode 100644 index 0000000..2e5d426 --- /dev/null +++ b/src/main/java/org/springframework/commons/repeat/RepeatCallback.java @@ -0,0 +1,44 @@ +/* + * 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.commons.repeat; + + +/** + * Callback interface for batch operations. Many simple processes will be able + * to use off-the-shelf implementations of this interface, enabling the + * application developer to concentrate on business logic. + * + * @see RepeatOperations + * + * @author Dave Syer + * + */ +public interface RepeatCallback { + + /** + * Implementations return true if they can continue processing - e.g. there + * is a data source that is not yet exhausted. Exceptions are not necessarily + * fatal - processing might continue depending on the Exception type and the + * implementation of the caller. + * + * @param context the current context passed in by the caller. + * @return an {@link RepeatStatus} which is continuable if there is (or may + * be) more data to process. + * @throws Exception if there is a problem with the processing. + */ + RepeatStatus doInIteration(RepeatContext context) throws Exception; +} diff --git a/src/main/java/org/springframework/commons/repeat/RepeatContext.java b/src/main/java/org/springframework/commons/repeat/RepeatContext.java new file mode 100644 index 0000000..099cf8a --- /dev/null +++ b/src/main/java/org/springframework/commons/repeat/RepeatContext.java @@ -0,0 +1,91 @@ +/* + * 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.commons.repeat; + +import org.springframework.core.AttributeAccessor; + +/** + * Base interface for context which controls the state and completion / + * termination of a batch step. A new context is created for each call to the + * {@link RepeatOperations}. Within a batch callback code can communicate via + * the {@link AttributeAccessor} interface. + * + * @author Dave Syer + * + * @see RepeatOperations#iterate(RepeatCallback) + * + */ +public interface RepeatContext extends AttributeAccessor { + + /** + * If batches are nested, then the inner batch will be created with the + * outer one as a parent. This is an accessor for the parent if it exists. + * + * @return the parent context or null if there is none + */ + RepeatContext getParent(); + + /** + * Public access to a counter for the number of operations attempted. + * + * @return the number of batch operations started. + */ + int getStartedCount(); + + /** + * Signal to the framework that the current batch should complete normally, + * independent of the current {@link CompletionPolicy}. + */ + void setCompleteOnly(); + + /** + * Public accessor for the complete flag. + */ + boolean isCompleteOnly(); + + /** + * Signal to the framework that the current batch should complete + * abnormally, independent of the current {@link CompletionPolicy}. + */ + void setTerminateOnly(); + + /** + * Public accessor for the termination flag. If this flag is set then the + * complete flag will also be. + */ + boolean isTerminateOnly(); + + /** + * Register a callback to be executed on close, associated with the + * attribute having the given name. The {@link Runnable} callback should not + * throw any exceptions. + * + * @param name the name of the attribute to associated this callback with. + * If this attribute is removed the callback should never be called. + * @param callback a {@link Runnable} to execute when the context is closed. + */ + void registerDestructionCallback(String name, Runnable callback); + + /** + * Allow resources to be cleared, especially in destruction callbacks. + * Implementations should ensure that any registered destruction callbacks + * are executed here, as long as the corresponding attribute is still + * available. + */ + void close(); + +} diff --git a/src/main/java/org/springframework/commons/repeat/RepeatException.java b/src/main/java/org/springframework/commons/repeat/RepeatException.java new file mode 100644 index 0000000..8e110b0 --- /dev/null +++ b/src/main/java/org/springframework/commons/repeat/RepeatException.java @@ -0,0 +1,31 @@ +/* + * 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.commons.repeat; + +import org.springframework.core.NestedRuntimeException; + +public class RepeatException extends NestedRuntimeException { + + public RepeatException(String msg) { + super(msg); + } + + public RepeatException(String msg, Throwable t) { + super(msg, t); + } + +} diff --git a/src/main/java/org/springframework/commons/repeat/RepeatOperations.java b/src/main/java/org/springframework/commons/repeat/RepeatOperations.java new file mode 100644 index 0000000..da49ee2 --- /dev/null +++ b/src/main/java/org/springframework/commons/repeat/RepeatOperations.java @@ -0,0 +1,47 @@ +/* + * 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.commons.repeat; + + +/** + * The main interface providing access to batch operations. The batch client is + * the {@link RepeatCallback}, where a single item or record is processed. The + * batch behaviour, boundary conditions, transactions etc, are dealt with by the + * {@link RepeatOperations} in such as way that the client does not need to know + * about them. The client may have access to framework abstractions, like + * template data sources, but these should work the same whether they are in a + * batch or not. + * + * @author Dave Syer + * + */ +public interface RepeatOperations { + + /** + * Execute the callback repeatedly, until a decision can be made to + * complete. The decision about how many times to execute or when to + * complete, and what to do in the case of an error is delegated to a + * {@link CompletionPolicy}. + * + * @param callback the batch callback. + * @return the aggregate of the result of all the callback operations. An + * indication of whether the {@link RepeatOperations} can continue + * processing if this method is called again. + */ + RepeatStatus iterate(RepeatCallback callback) throws RepeatException; + +} diff --git a/src/main/java/org/springframework/commons/repeat/RepeatStatus.java b/src/main/java/org/springframework/commons/repeat/RepeatStatus.java new file mode 100644 index 0000000..3a117ab --- /dev/null +++ b/src/main/java/org/springframework/commons/repeat/RepeatStatus.java @@ -0,0 +1,48 @@ +/* + * 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.commons.repeat; + +public enum RepeatStatus { + + /** + * Indicates that processing can continue. + */ + CONTINUABLE(true), + /** + * Indicates that processing is finished (either successful or unsuccessful) + */ + FINISHED(false); + + private final boolean continuable; + + private RepeatStatus(boolean continuable) { + this.continuable = continuable; + } + + public static RepeatStatus continueIf(boolean continuable) { + return continuable ? CONTINUABLE : FINISHED; + } + + public boolean isContinuable() { + return this == CONTINUABLE; + } + + public RepeatStatus and(boolean value) { + return value && continuable ? CONTINUABLE : FINISHED; + } + +} diff --git a/src/main/java/org/springframework/commons/retry/ExhaustedRetryException.java b/src/main/java/org/springframework/commons/retry/ExhaustedRetryException.java new file mode 100644 index 0000000..2a630fd --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/ExhaustedRetryException.java @@ -0,0 +1,29 @@ +/* + * 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.commons.retry; + +public class ExhaustedRetryException extends RetryException { + + public ExhaustedRetryException(String msg, Throwable cause) { + super(msg, cause); + } + + public ExhaustedRetryException(String msg) { + super(msg); + } + +} diff --git a/src/main/java/org/springframework/commons/retry/RecoveryCallback.java b/src/main/java/org/springframework/commons/retry/RecoveryCallback.java new file mode 100644 index 0000000..d158dc8 --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/RecoveryCallback.java @@ -0,0 +1,35 @@ +/* + * 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.commons.retry; + +/** + * Callback for stateful retry after all tries are exhausted. + * + * @author Dave Syer + * + * @since 1.1 + */ +public interface RecoveryCallback { + + /** + * @param context the current retry context + * @return an Object that can be used to replace the callback result that + * failed + * @throws Exception + */ + T recover(RetryContext context) throws Exception; + +} diff --git a/src/main/java/org/springframework/commons/retry/RetryCallback.java b/src/main/java/org/springframework/commons/retry/RetryCallback.java new file mode 100644 index 0000000..167fe68 --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/RetryCallback.java @@ -0,0 +1,37 @@ +/* + * 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.commons.retry; + +/** + * Callback interface for an operation that can be retried using a + * {@link RetryOperations}. + * + * @author Rob Harrop + * @author Dave Syer + */ +public interface RetryCallback { + + /** + * Execute an operation with retry semantics. Operations should generally be + * idempotent, but implementations may choose to implement compensation + * semantics when an operation is retried. + * @param context the current retry context. + * @return the result of the successful operation. + * @throws Exception if processing fails + */ + T doWithRetry(RetryContext context) throws Exception; +} diff --git a/src/main/java/org/springframework/commons/retry/RetryContext.java b/src/main/java/org/springframework/commons/retry/RetryContext.java new file mode 100644 index 0000000..d785066 --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/RetryContext.java @@ -0,0 +1,69 @@ +/* + * 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.commons.retry; + +import org.springframework.core.AttributeAccessor; + +/** + * Low-level access to ongoing retry operation. Normally not needed by clients, + * but can be used to alter the course of the retry, e.g. force an early + * termination. + * + * @author Dave Syer + * + */ +public interface RetryContext extends AttributeAccessor { + + /** + * Signal to the framework that no more attempts should be made to try or + * retry the current {@link RetryCallback}. + */ + void setExhaustedOnly(); + + /** + * Public accessor for the exhausted flag {@link #setExhaustedOnly()}. + * + * @return true if the flag has been set. + */ + boolean isExhaustedOnly(); + + /** + * Accessor for the parent context if retry blocks are nested. + * + * @return the parent or null if there is none. + */ + RetryContext getParent(); + + /** + * Counts the number of retry attempts. Before the first attempt this + * counter is zero, and before the first and subsequent attempts it should + * increment accordingly. + * + * @return the number of retries. + */ + int getRetryCount(); + + /** + * Accessor for the exception object that caused the current retry. + * + * @return the last exception that caused a retry, or possibly null. It will + * be null if this is the first attempt, but also if the enclosing policy + * decides not to provide it (e.g. because of concerns about memory usage). + */ + Throwable getLastThrowable(); + +} diff --git a/src/main/java/org/springframework/commons/retry/RetryException.java b/src/main/java/org/springframework/commons/retry/RetryException.java new file mode 100644 index 0000000..92900be --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/RetryException.java @@ -0,0 +1,31 @@ +/* + * 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.commons.retry; + +import org.springframework.core.NestedRuntimeException; + +public class RetryException extends NestedRuntimeException { + + public RetryException(String msg, Throwable cause) { + super(msg, cause); + } + + public RetryException(String msg) { + super(msg); + } + +} diff --git a/src/main/java/org/springframework/commons/retry/RetryListener.java b/src/main/java/org/springframework/commons/retry/RetryListener.java new file mode 100644 index 0000000..d8499c3 --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/RetryListener.java @@ -0,0 +1,62 @@ +/* + * 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.commons.retry; + + +/** + * Interface for listener that can be used to add behaviour to a retry. + * Implementations of {@link RetryOperations} can chose to issue callbacks to an + * interceptor during the retry lifecycle. + * + * @author Dave Syer + * + */ +public interface RetryListener { + + /** + * Called before the first attempt in a retry. For instance, implementers + * can set up state that is needed by the policies in the + * {@link RetryOperations}. The whole retry can be vetoed by returning + * false from this method, in which case a {@link TerminatedRetryException} + * will be thrown. + * + * @param context the current {@link RetryContext}. + * @param callback the current {@link RetryCallback}. + * @return true if the retry should proceed. + */ + boolean open(RetryContext context, RetryCallback callback); + + /** + * Called after the final attempt (successful or not). Allow the interceptor + * to clean up any resource it is holding before control returns to the + * retry caller. + * + * @param context the current {@link RetryContext}. + * @param callback the current {@link RetryCallback}. + * @param throwable the last exception that was thrown by the callback. + */ + void close(RetryContext context, RetryCallback callback, Throwable throwable); + + /** + * Called after every unsuccessful attempt at a retry. + * + * @param context the current {@link RetryContext}. + * @param callback the current {@link RetryCallback}. + * @param throwable the last exception that was thrown by the callback. + */ + void onError(RetryContext context, RetryCallback callback, Throwable throwable); +} diff --git a/src/main/java/org/springframework/commons/retry/RetryOperations.java b/src/main/java/org/springframework/commons/retry/RetryOperations.java new file mode 100644 index 0000000..f77e825 --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/RetryOperations.java @@ -0,0 +1,89 @@ +/* + * 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.commons.retry; + +import org.springframework.commons.retry.support.DefaultRetryState; + +/** + * Defines the basic set of operations implemented by {@link RetryOperations} to + * execute operations with configurable retry behaviour. + * + * @author Rob Harrop + * @author Dave Syer + */ +public interface RetryOperations { + + /** + * Execute the supplied {@link RetryCallback} with the configured retry + * semantics. See implementations for configuration details. + * + * @return the value returned by the {@link RetryCallback} upon successful + * invocation. + * @throws Exception any {@link Exception} raised by the + * {@link RetryCallback} upon unsuccessful retry. + */ + T execute(RetryCallback retryCallback) throws Exception; + + /** + * Execute the supplied {@link RetryCallback} with a fallback on exhausted + * retry to the {@link RecoveryCallback}. See implementations for + * configuration details. + * + * @return the value returned by the {@link RetryCallback} upon successful + * invocation, and that returned by the {@link RecoveryCallback} otherwise. + * @throws Exception any {@link Exception} raised by the + * {@link RecoveryCallback} upon unsuccessful retry. + */ + T execute(RetryCallback retryCallback, RecoveryCallback recoveryCallback) throws Exception; + + /** + * A simple stateful retry. Execute the supplied {@link RetryCallback} with + * a target object for the attempt identified by the {@link DefaultRetryState}. + * Exceptions thrown by the callback are always propagated immediately so + * the state is required to be able to identify the previous attempt, if + * there is one - hence the state is required. Normal patterns would see + * this method being used inside a transaction, where the callback might + * invalidate the transaction if it fails.

+ * + * See implementations for configuration details. + * + * @return the value returned by the {@link RetryCallback} upon successful + * invocation, and that returned by the {@link RecoveryCallback} otherwise. + * @throws Exception any {@link Exception} raised by the + * {@link RecoveryCallback}. + * @throws ExhaustedRetryException if the last attempt for this state has + * already been reached + */ + T execute(RetryCallback retryCallback, RetryState retryState) throws Exception, ExhaustedRetryException; + + /** + * A stateful retry with a recovery path. Execute the supplied + * {@link RetryCallback} with a fallback on exhausted retry to the + * {@link RecoveryCallback} and a target object for the retry attempt + * identified by the {@link DefaultRetryState}. + * + * @see #execute(RetryCallback, RetryState) + * + * @return the value returned by the {@link RetryCallback} upon successful + * invocation, and that returned by the {@link RecoveryCallback} otherwise. + * @throws Exception any {@link Exception} raised by the + * {@link RecoveryCallback} upon unsuccessful retry. + */ + T execute(RetryCallback retryCallback, RecoveryCallback recoveryCallback, RetryState retryState) + throws Exception; + +} diff --git a/src/main/java/org/springframework/commons/retry/RetryPolicy.java b/src/main/java/org/springframework/commons/retry/RetryPolicy.java new file mode 100644 index 0000000..273f870 --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/RetryPolicy.java @@ -0,0 +1,63 @@ +/* + * 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.commons.retry; + +/** + * A {@link RetryPolicy} is responsible for allocating and managing resources + * needed by {@link RetryOperations}. The {@link RetryPolicy} allows retry + * operations to be aware of their context. Context can be internal to the retry + * framework, e.g. to support nested retries. Context can also be external, and + * the {@link RetryPolicy} provides a uniform API for a range of different + * platforms for the external context. + * + * @author Dave Syer + * + */ +public interface RetryPolicy { + + /** + * @param context the current retry status + * @return true if the operation can proceed + */ + boolean canRetry(RetryContext context); + + /** + * Acquire resources needed for the retry operation. The callback is passed + * in so that marker interfaces can be used and a manager can collaborate + * with the callback to set up some state in the status token. + * @param parent the parent context if we are in a nested retry. + * + * @return a {@link RetryContext} object specific to this manager. + * + */ + RetryContext open(RetryContext parent); + + /** + * @param context a retry status created by the + * {@link #open(RetryContext)} method of this manager. + */ + void close(RetryContext context); + + /** + * Called once per retry attempt, after the callback fails. + * + * @param context the current status object. + * + */ + void registerThrowable(RetryContext context, Throwable throwable); + +} diff --git a/src/main/java/org/springframework/commons/retry/RetryState.java b/src/main/java/org/springframework/commons/retry/RetryState.java new file mode 100644 index 0000000..6dd1331 --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/RetryState.java @@ -0,0 +1,60 @@ +/* + * 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.commons.retry; + +/** + * Stateful retry is characterised by having to recognise the items that are + * being processed, so this interface is used primarily to provide a cache key in + * between failed attempts. It also provides a hints to the + * {@link RetryOperations} for optimisations to do with avoidable cache hits and + * switching to stateless retry if a rollback is not needed. + * + * @author Dave Syer + * + */ +public interface RetryState { + + /** + * Key representing the state for a retry attempt. Stateful retry is + * characterised by having to recognise the items that are being processed, + * so this value is used as a cache key in between failed attempts. + * + * @return the key that this state represents + */ + Object getKey(); + + /** + * Indicate whether a cache lookup can be avoided. If the key is known ahead + * of the retry attempt to be fresh (i.e. has never been seen before) then a + * cache lookup can be avoided if this flag is true. + * + * @return true if the state does not require an explicit check for the key + */ + boolean isForceRefresh(); + + /** + * Check whether this exception requires a rollback. The default is always + * true, which is conservative, so this method provides an optimisation for + * switching to stateless retry if there is an exception for which rollback + * is unnecessary. Example usage would be for a stateful retry to specify a + * validation exception as not for rollback. + * + * @param exception the exception that caused a retry attempt to fail + * @return true if this exception should cause a rollback + */ + boolean rollbackFor(Throwable exception); + +} \ No newline at end of file diff --git a/src/main/java/org/springframework/commons/retry/RetryStatistics.java b/src/main/java/org/springframework/commons/retry/RetryStatistics.java new file mode 100644 index 0000000..098220d --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/RetryStatistics.java @@ -0,0 +1,64 @@ +/* + * 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.commons.retry; + +/** + * Interface for statistics reporting of retry attempts. Counts the number of + * retry attempts, successes, errors (including retries), and aborts. + * + * @author Dave Syer + * + */ +public interface RetryStatistics { + + /** + * @return the number of completed retry attempts (successful or not). + */ + int getCompleteCount(); + + /** + * Get the number of times a retry block has been entered, irrespective of + * how many times the operation was retried. + * + * @return the number of retry blocks started. + */ + int getStartedCount(); + + /** + * Get the number of errors detected, whether or not they resulted in a + * retry. + * + * @return the number of errors detected. + */ + int getErrorCount(); + + /** + * Get the number of times a block failed to complete successfully, even + * after retry. + * + * @return the number of retry attempts that failed overall. + */ + int getAbortCount(); + + /** + * Get an identifier for the retry block for reporting purposes. + * + * @return an identifier for the block. + */ + String getName(); + +} diff --git a/src/main/java/org/springframework/commons/retry/TerminatedRetryException.java b/src/main/java/org/springframework/commons/retry/TerminatedRetryException.java new file mode 100644 index 0000000..6642bb7 --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/TerminatedRetryException.java @@ -0,0 +1,29 @@ +/* + * 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.commons.retry; + +public class TerminatedRetryException extends RetryException { + + public TerminatedRetryException(String msg, Throwable cause) { + super(msg, cause); + } + + public TerminatedRetryException(String msg) { + super(msg); + } + +} diff --git a/src/main/java/org/springframework/commons/retry/backoff/BackOffContext.java b/src/main/java/org/springframework/commons/retry/backoff/BackOffContext.java new file mode 100644 index 0000000..6f445a2 --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/backoff/BackOffContext.java @@ -0,0 +1,25 @@ +/* + * 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.commons.retry.backoff; + +/** + * @author Rob Harrop + * @since 2.1 + */ +public interface BackOffContext { + +} diff --git a/src/main/java/org/springframework/commons/retry/backoff/BackOffInterruptedException.java b/src/main/java/org/springframework/commons/retry/backoff/BackOffInterruptedException.java new file mode 100644 index 0000000..9a84e1b --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/backoff/BackOffInterruptedException.java @@ -0,0 +1,38 @@ +/* + * 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.commons.retry.backoff; + +import org.springframework.commons.retry.RetryException; + +/** + * Exception class signifiying that an attempt to back off using a + * {@link BackOffPolicy} was interrupted, most likely by an + * {@link InterruptedException} during a call to {@link Thread#sleep(long)}. + * + * @author Rob Harrop + * @since 2.1 + */ +public class BackOffInterruptedException extends RetryException { + + public BackOffInterruptedException(String msg) { + super(msg); + } + + public BackOffInterruptedException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/src/main/java/org/springframework/commons/retry/backoff/BackOffPolicy.java b/src/main/java/org/springframework/commons/retry/backoff/BackOffPolicy.java new file mode 100644 index 0000000..14c7857 --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/backoff/BackOffPolicy.java @@ -0,0 +1,61 @@ +/* + * 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.commons.retry.backoff; + +import org.springframework.commons.retry.RetryContext; + +/** + * Strategy interface to control back off between attempts in a single + * {@link org.springframework.commons.retry.support.RetryTemplate retry operation}. + *

Implementations are expected to be thread-safe and should be designed + * for concurrent access. Configuration for each implementation is also expected + * to be thread-safe but need not be suitable for high load concurrent access. + *

For each block of retry operations the {@link #start} method is called + * and implementations can return an implementation-specific + * {@link BackOffContext} that can be used to track state through subsequent + * back off invocations.

Each back off process is handled via a call to + * {@link #backOff}. The + * {@link org.springframework.commons.retry.support.RetryTemplate} will pass in + * the corresponding {@link BackOffContext} object created by the call to + * {@link #start}. + * + * @author Rob Harrop + * @author Dave Syer + */ +public interface BackOffPolicy { + + /** + * Start a new block of back off operations. Implementations can choose to + * pause when this method is called, but normally it returns immediately. + * + * @param context the current retry context, which might contain information + * that we can use to decide how to proceed. + * @return the implementation-specific {@link BackOffContext} or 'null'. + */ + BackOffContext start(RetryContext context); + + /** + * Back off/pause in an implementation-specific fashion. The passed in + * {@link BackOffContext} corresponds to the one created by the call to + * {@link #start} for a given retry operation set. + * + * @throws BackOffInterruptedException if the attempt at back off is + * interrupted. + */ + void backOff(BackOffContext backOffContext) throws BackOffInterruptedException; + +} diff --git a/src/main/java/org/springframework/commons/retry/backoff/ExponentialBackOffPolicy.java b/src/main/java/org/springframework/commons/retry/backoff/ExponentialBackOffPolicy.java new file mode 100644 index 0000000..b286e3c --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/backoff/ExponentialBackOffPolicy.java @@ -0,0 +1,192 @@ +/* + * 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.commons.retry.backoff; + +import org.springframework.commons.retry.RetryContext; +import org.springframework.util.ClassUtils; + +/** + * Implementation of {@link BackOffPolicy} that increases the back off period + * for each retry attempt in a given set using the {@link Math#exp(double) + * exponential} function. + *

+ * This implementation is thread-safe and suitable for concurrent access. + * Modifications to the configuration do not affect any retry sets that are + * already in progress. + *

+ * The {@link #setInitialInterval(long)} property controls the initial value + * passed to {@link Math#exp(double)} and the {@link #setMultiplier(double)} + * property controls by how much this value is increased for each subsequent + * attempt. + * + * @author Rob Harrop + * @author Dave Syer + */ +public class ExponentialBackOffPolicy implements BackOffPolicy { + + /** + * The default 'initialInterval' value - 100 millisecs. Coupled with the + * default 'multiplier' value this gives a useful initial spread of pauses + * for 1-5 retries. + */ + public static final long DEFAULT_INITIAL_INTERVAL = 100L; + + /** + * The default maximum backoff time (30 seconds). + */ + public static final long DEFAULT_MAX_INTERVAL = 30000L; + + /** + * The default 'multiplier' value - value 2 (100% increase per backoff). + */ + public static final double DEFAULT_MULTIPLIER = 2; + + /** + * The initial sleep interval. + */ + private volatile long initialInterval = DEFAULT_INITIAL_INTERVAL; + + /** + * The maximum value of the backoff period in milliseconds. + */ + private volatile long maxInterval = DEFAULT_MAX_INTERVAL; + + /** + * The value to increment the exp seed with for each retry attempt. + */ + private volatile double multiplier = DEFAULT_MULTIPLIER; + + private Sleeper sleeper = new ObjectWaitSleeper(); + + /** + * 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 initial sleep interval value. Default is 100 + * millisecond. Cannot be set to a value less than one. + */ + public void setInitialInterval(long initialInterval) { + this.initialInterval = (initialInterval > 1 ? initialInterval : 1); + } + + /** + * Set the multiplier value. Default is '2.0'. Hint: do not use + * values much in excess of 1.0 (or the backoff will get very long very + * fast). + */ + public void setMultiplier(double multiplier) { + this.multiplier = (multiplier > 1.0 ? multiplier : 1.0); + } + + /** + * Setter for maximum back off period. Default is 30000 (30 seconds). the + * value will be reset to 1 if this method is called with a value less than + * 1. Set this to avoid infinite waits if backing off a large number of + * times (or if the multiplier is set too high). + * + * @param maxInterval in milliseconds. + */ + public void setMaxInterval(long maxInterval) { + this.maxInterval = maxInterval > 0 ? maxInterval : 1; + } + + /** + * The initial period to sleep on the first backoff. + * @return the initial interval + */ + public long getInitialInterval() { + return initialInterval; + } + + /** + * The maximum interval to sleep for. Defaults to 30 seconds. + * + * @return the maximum interval. + */ + public long getMaxInterval() { + return maxInterval; + } + + /** + * The multiplier to use to generate the next backoff interval from the + * last. + * + * @return the multiplier in use + */ + public double getMultiplier() { + return multiplier; + } + + /** + * Returns a new instance of {@link BackOffContext} configured with the + * 'expSeed' and 'increment' values. + */ + public BackOffContext start(RetryContext context) { + return new ExponentialBackOffContext(this.initialInterval, this.multiplier, this.maxInterval); + } + + /** + * Pause for a length of time equal to ' + * exp(backOffContext.expSeed)'. + */ + public void backOff(BackOffContext backOffContext) throws BackOffInterruptedException { + ExponentialBackOffContext context = (ExponentialBackOffContext) backOffContext; + try { + sleeper.sleep(context.getSleepAndIncrement()); + } + catch (InterruptedException e) { + throw new BackOffInterruptedException("Thread interrupted while sleeping", e); + } + } + + private static class ExponentialBackOffContext implements BackOffContext { + + private final double multiplier; + + private long interval; + + private long maxInterval; + + public ExponentialBackOffContext(long expSeed, double multiplier, long maxInterval) { + this.interval = expSeed; + this.multiplier = multiplier; + this.maxInterval = maxInterval; + } + + public synchronized long getSleepAndIncrement() { + long sleep = this.interval; + if (sleep > maxInterval) { + sleep = (long) maxInterval; + } + else { + this.interval *= this.multiplier; + } + return sleep; + } + } + + public String toString() { + return ClassUtils.getShortName(getClass()) + "[initialInterval=" + initialInterval + ", multiplier=" + + multiplier + ", maxInterval=" + maxInterval + "]"; + } + +} diff --git a/src/main/java/org/springframework/commons/retry/backoff/FixedBackOffPolicy.java b/src/main/java/org/springframework/commons/retry/backoff/FixedBackOffPolicy.java new file mode 100644 index 0000000..ec44217 --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/backoff/FixedBackOffPolicy.java @@ -0,0 +1,81 @@ +/* + * 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.commons.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. + * @author Rob Harrop + * @author Dave Syer + */ +public class FixedBackOffPolicy extends StatelessBackOffPolicy { + + /** + * Default back off period - 1000ms. + */ + private static final long DEFAULT_BACK_OFF_PERIOD = 1000L; + + /** + * The back off period in milliseconds. Defaults to 1000ms. + */ + private volatile long backOffPeriod = DEFAULT_BACK_OFF_PERIOD; + + + private Sleeper sleeper = new ObjectWaitSleeper(); + + /** + * 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 back off period in milliseconds. Cannot be < 1. Default value + * is 1000ms. + */ + public void setBackOffPeriod(long backOffPeriod) { + this.backOffPeriod = (backOffPeriod > 0 ? backOffPeriod : 1); + } + + /** + * The backoff period in milliseconds. + * @return the backoff period + */ + public long getBackOffPeriod() { + return backOffPeriod; + } + + /** + * Pause for the {@link #setBackOffPeriod(long)}. + * @throws BackOffInterruptedException if interrupted during sleep. + */ + protected void doBackOff() throws BackOffInterruptedException { + try { + sleeper.sleep(backOffPeriod); + } + catch (InterruptedException e) { + throw new BackOffInterruptedException("Thread interrupted while sleeping", e); + } + } +} diff --git a/src/main/java/org/springframework/commons/retry/backoff/NoBackOffPolicy.java b/src/main/java/org/springframework/commons/retry/backoff/NoBackOffPolicy.java new file mode 100644 index 0000000..66c99d0 --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/backoff/NoBackOffPolicy.java @@ -0,0 +1,31 @@ +/* + * 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.commons.retry.backoff; + + +/** + * Implementation of {@link BackOffPolicy} that performs a no-op and as such all + * retry operation in a given set proceed one after the other with no pause. + * + * @author Rob Harrop + * @since 2.1 + */ +public class NoBackOffPolicy extends StatelessBackOffPolicy { + + protected void doBackOff() throws BackOffInterruptedException { + } +} diff --git a/src/main/java/org/springframework/commons/retry/backoff/ObjectWaitSleeper.java b/src/main/java/org/springframework/commons/retry/backoff/ObjectWaitSleeper.java new file mode 100644 index 0000000..10a88ad --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/backoff/ObjectWaitSleeper.java @@ -0,0 +1,37 @@ +/* + * 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.commons.retry.backoff; + +/** + * Simple {@link Sleeper} implementation that just waits on a local Object. + * + * @author Dave Syer + * + */ +public class ObjectWaitSleeper implements Sleeper { + + /* + * (non-Javadoc) + * @see org.springframework.commons.retry.backoff.Sleeper#sleep(long) + */ + public void sleep(long backOffPeriod) throws InterruptedException { + Object mutex = new Object(); + synchronized (mutex) { + mutex.wait(backOffPeriod); + } + } + +} diff --git a/src/main/java/org/springframework/commons/retry/backoff/Sleeper.java b/src/main/java/org/springframework/commons/retry/backoff/Sleeper.java new file mode 100644 index 0000000..8eccf45 --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/backoff/Sleeper.java @@ -0,0 +1,33 @@ +/* + * 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.commons.retry.backoff; + +/** + * Strategy interface for backoff policies to delegate the pausing of execution. + * + * @author Dave Syer + * + */ +public interface Sleeper { + + /** + * Pause for the specified period using whatever means available. + * + * @param backOffPeriod + */ + void sleep(long backOffPeriod) throws InterruptedException; + +} diff --git a/src/main/java/org/springframework/commons/retry/backoff/StatelessBackOffPolicy.java b/src/main/java/org/springframework/commons/retry/backoff/StatelessBackOffPolicy.java new file mode 100644 index 0000000..14ee264 --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/backoff/StatelessBackOffPolicy.java @@ -0,0 +1,51 @@ +/* + * 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.commons.retry.backoff; + +import org.springframework.commons.retry.RetryContext; + +/** + * Simple base class for {@link BackOffPolicy} implementations that maintain no + * state across invocations. + * + * @author Rob Harrop + * @author Dave Syer + */ +public abstract class StatelessBackOffPolicy implements BackOffPolicy { + + /** + * Delegates directly to the {@link #doBackOff()} method without passing on + * the {@link BackOffContext} argument which is not needed for stateless + * implementations. + */ + public final void backOff(BackOffContext backOffContext) throws BackOffInterruptedException { + doBackOff(); + } + + /** + * Returns 'null'. Subclasses can add behaviour, e.g. + * initial sleep before first attempt. + */ + public BackOffContext start(RetryContext status) { + return null; + } + + /** + * Sub-classes should implement this method to perform the actual back off. + */ + protected abstract void doBackOff() throws BackOffInterruptedException; +} diff --git a/src/main/java/org/springframework/commons/retry/context/RetryContextSupport.java b/src/main/java/org/springframework/commons/retry/context/RetryContextSupport.java new file mode 100644 index 0000000..0c5292a --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/context/RetryContextSupport.java @@ -0,0 +1,83 @@ +/* + * 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.commons.retry.context; + +import org.springframework.commons.retry.RetryContext; +import org.springframework.commons.retry.RetryPolicy; +import org.springframework.core.AttributeAccessorSupport; + +public class RetryContextSupport extends AttributeAccessorSupport implements RetryContext { + + private boolean terminate = false; + + private int count; + + private Throwable lastException; + + private RetryContext parent; + + public RetryContextSupport(RetryContext parent) { + super(); + this.parent = parent; + } + + public RetryContext getParent() { + return this.parent; + } + + public boolean isExhaustedOnly() { + return terminate; + } + + public void setExhaustedOnly() { + terminate = true; + } + + public int getRetryCount() { + return count; + } + + public Throwable getLastThrowable() { + return lastException; + } + + /** + * Set the exception for the public interface {@link RetryContext}, and + * also increment the retry count if the throwable is non-null.
+ * + * All {@link RetryPolicy} implementations should use this method when they + * register the throwable. It should only be called once per retry attempt + * because it increments a counter.
+ * + * Use of this method is not enforced by the framework - it is a service + * provider contract for authors of policies. + * + * @param throwable the exception that caused the current retry attempt to + * fail. + */ + public void registerThrowable(Throwable throwable) { + this.lastException = throwable; + if (throwable != null) + count++; + } + + @Override + public String toString() { + return String.format("[RetryContext: count=%d, lastException=%s, exhausted=%b]", count, lastException, terminate); + } + +} diff --git a/src/main/java/org/springframework/commons/retry/interceptor/MethodArgumentsKeyGenerator.java b/src/main/java/org/springframework/commons/retry/interceptor/MethodArgumentsKeyGenerator.java new file mode 100644 index 0000000..fd15b31 --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/interceptor/MethodArgumentsKeyGenerator.java @@ -0,0 +1,36 @@ +/* + * 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.commons.retry.interceptor; + +/** + * Interface that allows method parameters to be identified and tagged by a + * unique key. + * + * @author Dave Syer + * + */ +public interface MethodArgumentsKeyGenerator { + + /** + * Get a unique identifier for the item that can be used to cache it between + * calls if necessary, and then identify it later. + * + * @param item the current item. + * @return a unique identifier. + */ + Object getKey(Object[] item); + +} diff --git a/src/main/java/org/springframework/commons/retry/interceptor/MethodInvocationRecoverer.java b/src/main/java/org/springframework/commons/retry/interceptor/MethodInvocationRecoverer.java new file mode 100644 index 0000000..376a6b9 --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/interceptor/MethodInvocationRecoverer.java @@ -0,0 +1,40 @@ +/* + * 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.commons.retry.interceptor; + + +/** + * Strategy interface for recovery action when processing of an item fails.
+ * + * @author Dave Syer + */ +public interface MethodInvocationRecoverer { + + /** + * Recover gracefully from an error. Clients can call this if processing of + * the item throws an unexpected exception. Caller can use the return value + * to decide whether to try more corrective action or perhaps throw an + * exception. + * + * @param args + * the arguments for the method invocation that failed. + * @param cause + * the cause of the failure that led to this recovery. + * @return the value to be returned to the caller + */ + T recover(Object[] args, Throwable cause); +} diff --git a/src/main/java/org/springframework/commons/retry/interceptor/NewMethodArgumentsIdentifier.java b/src/main/java/org/springframework/commons/retry/interceptor/NewMethodArgumentsIdentifier.java new file mode 100644 index 0000000..92b1f2b --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/interceptor/NewMethodArgumentsIdentifier.java @@ -0,0 +1,37 @@ +/* + * 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.commons.retry.interceptor; + +/** + * Strategy interface to distinguish new arguments from ones that have been + * processed before, e.g. by examining a message flag. + * + * @author Dave Syer + * + */ +public interface NewMethodArgumentsIdentifier { + + /** + * Inspect the arguments and determine if they have never been processed + * before. The safest choice when the answer is indeterminate is 'false'. + * + * @param args the current method arguments. + * @return true if the item is known to have never been processed before. + */ + boolean isNew(Object[] args); + +} diff --git a/src/main/java/org/springframework/commons/retry/interceptor/RetryOperationsInterceptor.java b/src/main/java/org/springframework/commons/retry/interceptor/RetryOperationsInterceptor.java new file mode 100644 index 0000000..377482f --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/interceptor/RetryOperationsInterceptor.java @@ -0,0 +1,86 @@ +/* + * 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.commons.retry.interceptor; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.ProxyMethodInvocation; +import org.springframework.commons.retry.RetryCallback; +import org.springframework.commons.retry.RetryContext; +import org.springframework.commons.retry.RetryOperations; +import org.springframework.commons.retry.support.RetryTemplate; +import org.springframework.util.Assert; + +/** + * A {@link MethodInterceptor} that can be used to automatically retry calls to + * a method on a service if it fails. The injected {@link RetryOperations} is + * used to control the number of retries. By default it will retry a fixed + * number of times, according to the defaults in {@link RetryTemplate}.
+ * + * Hint about transaction boundaries. If you want to retry a failed transaction + * you need to make sure that the transaction boundary is inside the retry, + * otherwise the successful attempt will roll back with the whole transaction. + * If the method being intercepted is also transactional, then use the ordering + * hints in the advice declarations to ensure that this one is before the + * transaction interceptor in the advice chain. + * + * @author Rob Harrop + * @author Dave Syer + */ +public class RetryOperationsInterceptor implements MethodInterceptor { + + private RetryOperations retryOperations = new RetryTemplate(); + + public void setRetryOperations(RetryOperations retryTemplate) { + Assert.notNull(retryTemplate, "'retryOperations' cannot be null."); + this.retryOperations = retryTemplate; + } + + public Object invoke(final MethodInvocation invocation) throws Throwable { + + return this.retryOperations.execute(new RetryCallback() { + + public Object doWithRetry(RetryContext context) throws Exception { + + /* + * If we don't copy the invocation carefully it won't keep a + * reference to the other interceptors in the chain. We don't + * have a choice here but to specialise to + * ReflectiveMethodInvocation (but how often would another + * implementation come along?). + */ + if (invocation instanceof ProxyMethodInvocation) { + try { + return ((ProxyMethodInvocation) invocation) + .invocableClone().proceed(); + } + catch (Exception e) { + throw e; + } catch (Error e) { + throw e; + } catch (Throwable e) { + throw new IllegalStateException(e); + } + } else { + throw new IllegalStateException( + "MethodInvocation of the wrong type detected - this should not happen with Spring AOP, so please raise an issue if you see this exception"); + } + } + + }); + } +} diff --git a/src/main/java/org/springframework/commons/retry/interceptor/StatefulRetryOperationsInterceptor.java b/src/main/java/org/springframework/commons/retry/interceptor/StatefulRetryOperationsInterceptor.java new file mode 100644 index 0000000..259fbb6 --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/interceptor/StatefulRetryOperationsInterceptor.java @@ -0,0 +1,214 @@ +/* + * 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.commons.retry.interceptor; + +import java.util.Arrays; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.commons.retry.ExhaustedRetryException; +import org.springframework.commons.retry.RecoveryCallback; +import org.springframework.commons.retry.RetryCallback; +import org.springframework.commons.retry.RetryContext; +import org.springframework.commons.retry.RetryOperations; +import org.springframework.commons.retry.RetryState; +import org.springframework.commons.retry.policy.NeverRetryPolicy; +import org.springframework.commons.retry.support.DefaultRetryState; +import org.springframework.commons.retry.support.RetryTemplate; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * A {@link MethodInterceptor} that can be used to automatically retry calls to + * a method on a service if it fails. The argument to the service method is + * treated as an item to be remembered in case the call fails. So the retry + * operation is stateful, and the item that failed is tracked by its unique key + * (via {@link MethodArgumentsKeyGenerator}) until the retry is exhausted, at + * which point the {@link MethodInvocationRecoverer} is called.
+ * + * The main use case for this is where the service is transactional, via a + * transaction interceptor on the interceptor chain. In this case the retry (and + * recovery on exhausted) always happens in a new transaction.
+ * + * The injected {@link RetryOperations} is used to control the number of + * retries. By default it will retry a fixed number of times, according to the + * defaults in {@link RetryTemplate}.
+ * + * @author Dave Syer + */ +public class StatefulRetryOperationsInterceptor implements MethodInterceptor { + + private transient Log logger = LogFactory.getLog(getClass()); + + private MethodArgumentsKeyGenerator keyGenerator; + + private MethodInvocationRecoverer recoverer; + + private NewMethodArgumentsIdentifier newMethodArgumentsIdentifier; + + private RetryOperations retryOperations; + + public void setRetryOperations(RetryOperations retryTemplate) { + Assert.notNull(retryTemplate, "'retryOperations' cannot be null."); + this.retryOperations = retryTemplate; + } + + /** + * + */ + public StatefulRetryOperationsInterceptor() { + super(); + RetryTemplate retryTemplate = new RetryTemplate(); + retryTemplate.setRetryPolicy(new NeverRetryPolicy()); + retryOperations = retryTemplate; + } + + /** + * Public setter for the {@link MethodInvocationRecoverer} to use if the + * retry is exhausted. The recoverer should be able to return an object of + * the same type as the target object because its return value will be used + * to return to the caller in the case of a recovery.
+ * + * If no recoverer is set then an exhausted retry will result in an + * {@link ExhaustedRetryException}. + * + * @param recoverer the {@link MethodInvocationRecoverer} to set + */ + public void setRecoverer(MethodInvocationRecoverer recoverer) { + this.recoverer = recoverer; + } + + public void setKeyGenerator(MethodArgumentsKeyGenerator keyGenerator) { + this.keyGenerator = keyGenerator; + } + + /** + * Public setter for the {@link NewMethodArgumentsIdentifier}. Only set this + * if the arguments to the intercepted method can be inspected to find out + * if they have never been processed before. + * @param newMethodArgumentsIdentifier the + * {@link NewMethodArgumentsIdentifier} to set + */ + public void setNewItemIdentifier(NewMethodArgumentsIdentifier newMethodArgumentsIdentifier) { + this.newMethodArgumentsIdentifier = newMethodArgumentsIdentifier; + } + + /** + * Wrap the method invocation in a stateful retry with the policy and other + * helpers provided. If there is a failure the exception will generally be + * re-thrown. The only time it is not re-thrown is when retry is exhausted + * and the recovery path is taken (though the + * {@link MethodInvocationRecoverer} provided if there is one). In that case + * the value returned from the method invocation will be the value returned + * by the recoverer (so the return type for that should be the same as the + * intercepted method). + * + * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation) + * @see MethodInvocationRecoverer#recover(Object[], Throwable) + * + * @throws ExhaustedRetryException if the retry is exhausted and no + * {@link MethodInvocationRecoverer} is provided. + */ + public Object invoke(final MethodInvocation invocation) throws Throwable { + + logger.debug("Executing proxied method in stateful retry: " + invocation.getStaticPart() + "(" + + ObjectUtils.getIdentityHexString(invocation) + ")"); + + Object[] args = invocation.getArguments(); + Assert.state(args.length > 0, "Stateful retry applied to method that takes no arguments: " + + invocation.getStaticPart()); + Object arg = args; + if (args.length == 1) { + arg = args[0]; + } + final Object item = arg; + + RetryState retryState = new DefaultRetryState(keyGenerator != null ? keyGenerator.getKey(args) : item, + newMethodArgumentsIdentifier != null ? newMethodArgumentsIdentifier.isNew(args) : false); + + Object result = retryOperations.execute(new MethodInvocationRetryCallback(invocation), + new ItemRecovererCallback(args, recoverer), retryState); + + logger.debug("Exiting proxied method in stateful retry with result: (" + result + ")"); + + return result; + + } + + /** + * @author Dave Syer + * + */ + private static final class MethodInvocationRetryCallback implements RetryCallback { + /** + * + */ + private final MethodInvocation invocation; + + /** + * @param invocation + */ + private MethodInvocationRetryCallback(MethodInvocation invocation) { + this.invocation = invocation; + } + + public Object doWithRetry(RetryContext context) throws Exception { + try { + return invocation.proceed(); + } + catch (Exception e) { + throw e; + } + catch (Error e) { + throw e; + } + catch (Throwable e) { + throw new IllegalStateException(e); + } + } + } + + /** + * @author Dave Syer + * + */ + private static final class ItemRecovererCallback implements RecoveryCallback { + + private final Object[] args; + + private final MethodInvocationRecoverer recoverer; + + /** + * @param args the item that failed. + */ + private ItemRecovererCallback(Object[] args, MethodInvocationRecoverer recoverer) { + this.args = Arrays.asList(args).toArray(); + this.recoverer = recoverer; + } + + public Object recover(RetryContext context) { + if (recoverer != null) { + return recoverer.recover(args, context.getLastThrowable()); + } + throw new ExhaustedRetryException("Retry was exhausted but there was no recovery path."); + } + + } + +} diff --git a/src/main/java/org/springframework/commons/retry/listener/RetryListenerSupport.java b/src/main/java/org/springframework/commons/retry/listener/RetryListenerSupport.java new file mode 100644 index 0000000..a4b84f1 --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/listener/RetryListenerSupport.java @@ -0,0 +1,41 @@ +/* + * 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.commons.retry.listener; + +import org.springframework.commons.retry.RetryCallback; +import org.springframework.commons.retry.RetryContext; +import org.springframework.commons.retry.RetryListener; + +/** + * Empty method implementation of {@link RetryListener}. + * + * @author Dave Syer + * + */ +public class RetryListenerSupport implements RetryListener { + + public void close(RetryContext context, RetryCallback callback, Throwable throwable) { + } + + public void onError(RetryContext context, RetryCallback callback, Throwable throwable) { + } + + public boolean open(RetryContext context, RetryCallback callback) { + return true; + } + +} diff --git a/src/main/java/org/springframework/commons/retry/policy/AlwaysRetryPolicy.java b/src/main/java/org/springframework/commons/retry/policy/AlwaysRetryPolicy.java new file mode 100644 index 0000000..369922d --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/policy/AlwaysRetryPolicy.java @@ -0,0 +1,40 @@ +/* + * 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.commons.retry.policy; + +import org.springframework.commons.retry.RetryContext; +import org.springframework.commons.retry.RetryPolicy; + +/** + * A {@link RetryPolicy} that always permits a retry. Can also be used as a base + * class for other policies, e.g. for test purposes as a stub. + * + * @author Dave Syer + * + */ +public class AlwaysRetryPolicy extends NeverRetryPolicy { + + /** + * Always returns true. + * + * @see org.springframework.commons.retry.RetryPolicy#canRetry(org.springframework.commons.retry.RetryContext) + */ + public boolean canRetry(RetryContext context) { + return true; + } + +} diff --git a/src/main/java/org/springframework/commons/retry/policy/CompositeRetryPolicy.java b/src/main/java/org/springframework/commons/retry/policy/CompositeRetryPolicy.java new file mode 100644 index 0000000..117ec71 --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/policy/CompositeRetryPolicy.java @@ -0,0 +1,133 @@ +/* + * 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.commons.retry.policy; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.commons.retry.RetryContext; +import org.springframework.commons.retry.RetryPolicy; +import org.springframework.commons.retry.context.RetryContextSupport; + +/** + * A {@link RetryPolicy} that composes a list of other policies and delegates + * calls to them in order. + * + * @author Dave Syer + * + */ +public class CompositeRetryPolicy implements RetryPolicy { + + RetryPolicy[] policies = new RetryPolicy[0]; + + /** + * Setter for policies. + * + * @param policies + */ + public void setPolicies(RetryPolicy[] policies) { + this.policies = Arrays.asList(policies).toArray(new RetryPolicy[policies.length]); + } + + /** + * Delegate to the policies that were in operation when the context was + * created. If any of them cannot retry then return false, oetherwise return + * true. + * + * @see org.springframework.commons.retry.RetryPolicy#canRetry(org.springframework.commons.retry.RetryContext) + */ + public boolean canRetry(RetryContext context) { + RetryContext[] contexts = ((CompositeRetryContext) context).contexts; + RetryPolicy[] policies = ((CompositeRetryContext) context).policies; + for (int i = 0; i < contexts.length; i++) { + if (!policies[i].canRetry(contexts[i])) { + return false; + } + } + return true; + } + + /** + * Delegate to the policies that were in operation when the context was + * created. If any of them fails to close the exception is propagated (and + * those later in the chain are closed before re-throwing). + * + * @see org.springframework.commons.retry.RetryPolicy#close(org.springframework.commons.retry.RetryContext) + */ + public void close(RetryContext context) { + RetryContext[] contexts = ((CompositeRetryContext) context).contexts; + RetryPolicy[] policies = ((CompositeRetryContext) context).policies; + RuntimeException exception = null; + for (int i = 0; i < contexts.length; i++) { + try { + policies[i].close(contexts[i]); + } + catch (RuntimeException e) { + if (exception == null) { + exception = e; + } + } + } + if (exception != null) { + throw exception; + } + } + + /** + * Creates a new context that copies the existing policies and keeps a list + * of the contexts from each one. + * + * @see org.springframework.commons.retry.RetryPolicy#open(RetryContext) + */ + public RetryContext open(RetryContext parent) { + List list = new ArrayList(); + for (int i = 0; i < policies.length; i++) { + list.add(policies[i].open(parent)); + } + return new CompositeRetryContext(parent, list); + } + + /** + * Delegate to the policies that were in operation when the context was + * created. + * + * @see org.springframework.commons.retry.RetryPolicy#close(org.springframework.commons.retry.RetryContext) + */ + public void registerThrowable(RetryContext context, Throwable throwable) { + RetryContext[] contexts = ((CompositeRetryContext) context).contexts; + RetryPolicy[] policies = ((CompositeRetryContext) context).policies; + for (int i = 0; i < contexts.length; i++) { + policies[i].registerThrowable(contexts[i], throwable); + } + ((RetryContextSupport) context).registerThrowable(throwable); + } + + private class CompositeRetryContext extends RetryContextSupport { + RetryContext[] contexts; + + RetryPolicy[] policies; + + public CompositeRetryContext(RetryContext parent, List contexts) { + super(parent); + this.contexts = contexts.toArray(new RetryContext[0]); + this.policies = CompositeRetryPolicy.this.policies; + } + + } + +} diff --git a/src/main/java/org/springframework/commons/retry/policy/ExceptionClassifierRetryPolicy.java b/src/main/java/org/springframework/commons/retry/policy/ExceptionClassifierRetryPolicy.java new file mode 100644 index 0000000..301ff19 --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/policy/ExceptionClassifierRetryPolicy.java @@ -0,0 +1,163 @@ +/* + * 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.commons.retry.policy; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.commons.classify.Classifier; +import org.springframework.commons.classify.ClassifierSupport; +import org.springframework.commons.classify.SubclassClassifier; +import org.springframework.commons.retry.RetryContext; +import org.springframework.commons.retry.RetryPolicy; +import org.springframework.commons.retry.context.RetryContextSupport; +import org.springframework.util.Assert; + +/** + * A {@link RetryPolicy} that dynamically adapts to one of a set of injected + * policies according to the value of the latest exception. + * + * @author Dave Syer + * + */ +public class ExceptionClassifierRetryPolicy implements RetryPolicy { + + private Classifier exceptionClassifier = new ClassifierSupport( + new NeverRetryPolicy()); + + /** + * Setter for policy map used to create a classifier. Either this property + * or the exception classifier directly should be set, but not both. + * + * @param policyMap a map of Throwable class to {@link RetryPolicy} that + * will be used to create a {@link Classifier} to locate a policy. + */ + public void setPolicyMap(Map, RetryPolicy> policyMap) { + SubclassClassifier subclassClassifier = new SubclassClassifier( + policyMap, (RetryPolicy) new NeverRetryPolicy()); + this.exceptionClassifier = subclassClassifier; + } + + /** + * Setter for an exception classifier. The classifier is responsible for + * translating exceptions to concrete retry policies. Either this property + * or the policy map should be used, but not both. + * + * @param exceptionClassifier ExceptionClassifier to use + */ + public void setExceptionClassifier(Classifier exceptionClassifier) { + this.exceptionClassifier = exceptionClassifier; + } + + /** + * Delegate to the policy currently activated in the context. + * + * @see org.springframework.commons.retry.RetryPolicy#canRetry(org.springframework.commons.retry.RetryContext) + */ + public boolean canRetry(RetryContext context) { + RetryPolicy policy = (RetryPolicy) context; + return policy.canRetry(context); + } + + /** + * Delegate to the policy currently activated in the context. + * + * @see org.springframework.commons.retry.RetryPolicy#close(org.springframework.commons.retry.RetryContext) + */ + public void close(RetryContext context) { + RetryPolicy policy = (RetryPolicy) context; + policy.close(context); + } + + /** + * Create an active context that proxies a retry policy by chosing a target + * from the policy map. + * + * @see org.springframework.commons.retry.RetryPolicy#open(RetryContext) + */ + public RetryContext open(RetryContext parent) { + return new ExceptionClassifierRetryContext(parent, exceptionClassifier).open(parent); + } + + /** + * Delegate to the policy currently activated in the context. + * + * @see org.springframework.commons.retry.RetryPolicy#registerThrowable(org.springframework.commons.retry.RetryContext, + * Throwable) + */ + public void registerThrowable(RetryContext context, Throwable throwable) { + RetryPolicy policy = (RetryPolicy) context; + policy.registerThrowable(context, throwable); + ((RetryContextSupport) context).registerThrowable(throwable); + } + + private static class ExceptionClassifierRetryContext extends RetryContextSupport implements RetryPolicy { + + final private Classifier exceptionClassifier; + + // Dynamic: depends on the latest exception: + private RetryPolicy policy; + + // Dynamic: depends on the policy: + private RetryContext context; + + final private Map contexts = new HashMap(); + + public ExceptionClassifierRetryContext(RetryContext parent, + Classifier exceptionClassifier) { + super(parent); + this.exceptionClassifier = exceptionClassifier; + } + + public boolean canRetry(RetryContext context) { + if (this.context == null) { + // there was no error yet + return true; + } + return policy.canRetry(this.context); + } + + public void close(RetryContext context) { + // Only close those policies that have been used (opened): + for (RetryPolicy policy : contexts.keySet()) { + policy.close(getContext(policy, context.getParent())); + } + } + + public RetryContext open(RetryContext parent) { + return this; + } + + public void registerThrowable(RetryContext context, Throwable throwable) { + policy = exceptionClassifier.classify(throwable); + Assert.notNull(policy, "Could not locate policy for exception=[" + throwable + "]."); + this.context = getContext(policy, context.getParent()); + policy.registerThrowable(this.context, throwable); + } + + private RetryContext getContext(RetryPolicy policy, RetryContext parent) { + RetryContext context = contexts.get(policy); + if (context == null) { + context = policy.open(parent); + contexts.put(policy, context); + } + return context; + } + + } + +} diff --git a/src/main/java/org/springframework/commons/retry/policy/MapRetryContextCache.java b/src/main/java/org/springframework/commons/retry/policy/MapRetryContextCache.java new file mode 100644 index 0000000..d41021c --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/policy/MapRetryContextCache.java @@ -0,0 +1,92 @@ +/* + * 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.commons.retry.policy; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.commons.retry.RetryContext; + +/** + * Map-based implementation of {@link RetryContextCache}. The map backing the + * cache of contexts is synchronized. + * + * @author Dave Syer + * + */ +public class MapRetryContextCache implements RetryContextCache { + + /** + * Default value for maximum capacity of the cache. This is set to a + * reasonably low value (4096) to avoid users inadvertently filling the + * cache with item keys that are inconsistent. + */ + public static final int DEFAULT_CAPACITY = 4096; + + private Map map = Collections.synchronizedMap(new HashMap()); + + private int capacity; + + /** + * Create a {@link MapRetryContextCache} with default capacity. + */ + public MapRetryContextCache() { + this(DEFAULT_CAPACITY); + } + + /** + * @param defaultCapacity + */ + public MapRetryContextCache(int defaultCapacity) { + super(); + this.capacity = defaultCapacity; + } + + /** + * Public setter for the capacity. Prevents the cache from growing + * unboundedly if items that fail are misidentified and two references to an + * identical item actually do not have the same key. This can happen when + * users implement equals and hashCode based on mutable fields, for + * instance. + * + * @param capacity the capacity to set + */ + public void setCapacity(int capacity) { + this.capacity = capacity; + } + + public boolean containsKey(Object key) { + return map.containsKey(key); + } + + public RetryContext get(Object key) { + return map.get(key); + } + + public void put(Object key, RetryContext context) { + if (map.size() >= capacity) { + throw new RetryCacheCapacityExceededException("Retry cache capacity limit breached. " + + "Do you need to re-consider the implementation of the key generator, " + + "or the equals and hashCode of the items that failed?"); + } + map.put(key, context); + } + + public void remove(Object key) { + map.remove(key); + } +} diff --git a/src/main/java/org/springframework/commons/retry/policy/NeverRetryPolicy.java b/src/main/java/org/springframework/commons/retry/policy/NeverRetryPolicy.java new file mode 100644 index 0000000..09d53c0 --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/policy/NeverRetryPolicy.java @@ -0,0 +1,99 @@ +/* + * 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.commons.retry.policy; + +import org.springframework.commons.retry.RetryContext; +import org.springframework.commons.retry.RetryPolicy; +import org.springframework.commons.retry.context.RetryContextSupport; + +/** + * A {@link RetryPolicy} that allows the first attempt but never permits a + * retry. Also be used as a base class for other policies, e.g. for test + * purposes as a stub. + * + * @author Dave Syer + * + */ +public class NeverRetryPolicy implements RetryPolicy { + + /** + * Returns false after the first exception. So there is always one try, and + * then the retry is prevented. + * + * @see org.springframework.commons.retry.RetryPolicy#canRetry(org.springframework.commons.retry.RetryContext) + */ + public boolean canRetry(RetryContext context) { + return !((NeverRetryContext) context).isFinished(); + } + + /** + * Do nothing. + * + * @see org.springframework.commons.retry.RetryPolicy#close(org.springframework.commons.retry.RetryContext) + */ + public void close(RetryContext context) { + // no-op + } + + /** + * Return a context that can respond to early termination requests, but does + * nothing else. + * + * @see org.springframework.commons.retry.RetryPolicy#open(RetryContext) + */ + public RetryContext open(RetryContext parent) { + return new NeverRetryContext(parent); + } + + /** + * Make the throwable available for downstream use through the context. + * @see org.springframework.commons.retry.RetryPolicy#registerThrowable(org.springframework.commons.retry.RetryContext, + * Throwable) + */ + public void registerThrowable(RetryContext context, Throwable throwable) { + ((NeverRetryContext) context).setFinished(); + ((RetryContextSupport) context).registerThrowable(throwable); + } + + /** + * Special context object for {@link NeverRetryPolicy}. Implements a flag + * with a similar function to {@link RetryContext#isExhaustedOnly()}, but + * kept separate so that if subclasses of {@link NeverRetryPolicy} need to + * they can modify the behaviour of + * {@link NeverRetryPolicy#canRetry(RetryContext)} without affecting + * {@link RetryContext#isExhaustedOnly()}. + * + * @author Dave Syer + * + */ + private static class NeverRetryContext extends RetryContextSupport { + private boolean finished = false; + + public NeverRetryContext(RetryContext parent) { + super(parent); + } + + public boolean isFinished() { + return finished; + } + + public void setFinished() { + this.finished = true; + } + } + +} diff --git a/src/main/java/org/springframework/commons/retry/policy/RetryCacheCapacityExceededException.java b/src/main/java/org/springframework/commons/retry/policy/RetryCacheCapacityExceededException.java new file mode 100644 index 0000000..e19db59 --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/policy/RetryCacheCapacityExceededException.java @@ -0,0 +1,52 @@ +/* + * 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.commons.retry.policy; + +import org.springframework.commons.retry.RetryException; + +/** + * Exception that indicates that a cache limit was exceeded. This is often a + * sign of badly or inconsistently implemented hashCode, equals in failed items. + * Items can then fail repeatedly and appear different to the cache, so they get + * added over and over again until a limit is reached and this exception is + * thrown. Consult the documentation of the {@link RetryContextCache} in use to + * determine how to increase the limit if appropriate. + * + * @author Dave Syer + * + */ +public class RetryCacheCapacityExceededException extends RetryException { + + /** + * Constructs a new instance with a message. + * + * @param message + */ + public RetryCacheCapacityExceededException(String message) { + super(message); + } + + /** + * Constructs a new instance with a message and nested exception. + * + * @param msg the exception message. + * + */ + public RetryCacheCapacityExceededException(String msg, Throwable nested) { + super(msg, nested); + } + +} diff --git a/src/main/java/org/springframework/commons/retry/policy/RetryContextCache.java b/src/main/java/org/springframework/commons/retry/policy/RetryContextCache.java new file mode 100644 index 0000000..0def72c --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/policy/RetryContextCache.java @@ -0,0 +1,40 @@ +/* + * 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.commons.retry.policy; + +import org.springframework.commons.retry.RetryContext; + +/** + * Simple map-like abstraction for stateful retry policies to use when storing + * and retrieving {@link RetryContext} instances. + * + * @author Dave Syer + * + * @see MapRetryContextCache + * + */ +public interface RetryContextCache { + + RetryContext get(Object key); + + void put(Object key, RetryContext context) throws RetryCacheCapacityExceededException; + + void remove(Object key); + + boolean containsKey(Object key); + +} diff --git a/src/main/java/org/springframework/commons/retry/policy/SimpleRetryPolicy.java b/src/main/java/org/springframework/commons/retry/policy/SimpleRetryPolicy.java new file mode 100644 index 0000000..ce2aec0 --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/policy/SimpleRetryPolicy.java @@ -0,0 +1,152 @@ +/* + * 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.commons.retry.policy; + +import java.util.Collections; +import java.util.Map; + +import org.springframework.commons.classify.BinaryExceptionClassifier; +import org.springframework.commons.retry.RetryContext; +import org.springframework.commons.retry.RetryPolicy; +import org.springframework.commons.retry.context.RetryContextSupport; + +/** + * + * Simple retry policy that retries a fixed number of times for a set of named + * exceptions (and subclasses). The number of attempts includes the initial try, + * so e.g. + * + *
+ * retryTemplate = new RetryTemplate(new SimpleRetryPolicy(3));
+ * retryTemplate.execute(callback);
+ * 
+ * + * will execute the callback at least once, and as many as 3 times. + * + * @author Dave Syer + * @author Rob Harrop + * + */ +public class SimpleRetryPolicy implements RetryPolicy { + + /** + * The default limit to the number of attempts for a new policy. + */ + public final static int DEFAULT_MAX_ATTEMPTS = 3; + + private volatile int maxAttempts; + + private BinaryExceptionClassifier retryableClassifier = new BinaryExceptionClassifier(false); + + /** + * Create a {@link SimpleRetryPolicy} with the default number of retry + * attempts. + */ + public SimpleRetryPolicy() { + this(DEFAULT_MAX_ATTEMPTS, Collections + ., Boolean> singletonMap(Exception.class, true)); + } + + /** + * Create a {@link SimpleRetryPolicy} with the specified number of retry + * attempts. + * + * @param maxAttempts + * @param retryableExceptions + */ + public SimpleRetryPolicy(int maxAttempts, Map, Boolean> retryableExceptions) { + super(); + this.maxAttempts = maxAttempts; + this.retryableClassifier = new BinaryExceptionClassifier(retryableExceptions); + } + + /** + * Setter for retry attempts. + * + * @param retryAttempts the number of attempts before a retry becomes + * impossible. + */ + public void setMaxAttempts(int retryAttempts) { + this.maxAttempts = retryAttempts; + } + + /** + * The maximum number of retry attempts before failure. + * + * @return the maximum number of attempts + */ + public int getMaxAttempts() { + return maxAttempts; + } + + /** + * Test for retryable operation based on the status. + * + * @see org.springframework.commons.retry.RetryPolicy#canRetry(org.springframework.commons.retry.RetryContext) + * + * @return true if the last exception was retryable and the number of + * attempts so far is less than the limit. + */ + public boolean canRetry(RetryContext context) { + Throwable t = context.getLastThrowable(); + return (t == null || retryForException(t)) && context.getRetryCount() < maxAttempts; + } + + /** + * @see org.springframework.commons.retry.RetryPolicy#close(RetryContext) + */ + public void close(RetryContext status) { + } + + /** + * Update the status with another attempted retry and the latest exception. + * + * @see RetryPolicy#registerThrowable(RetryContext, Throwable) + */ + public void registerThrowable(RetryContext context, Throwable throwable) { + SimpleRetryContext simpleContext = ((SimpleRetryContext) context); + simpleContext.registerThrowable(throwable); + } + + /** + * Get a status object that can be used to track the current operation + * according to this policy. Has to be aware of the latest exception and the + * number of attempts. + * + * @see org.springframework.commons.retry.RetryPolicy#open(RetryContext) + */ + public RetryContext open(RetryContext parent) { + return new SimpleRetryContext(parent); + } + + private static class SimpleRetryContext extends RetryContextSupport { + public SimpleRetryContext(RetryContext parent) { + super(parent); + } + } + + /** + * Delegates to an exception classifier. + * + * @param ex + * @return true if this exception or its ancestors have been registered as + * retryable. + */ + private boolean retryForException(Throwable ex) { + return retryableClassifier.classify(ex); + } +} diff --git a/src/main/java/org/springframework/commons/retry/policy/SoftReferenceMapRetryContextCache.java b/src/main/java/org/springframework/commons/retry/policy/SoftReferenceMapRetryContextCache.java new file mode 100644 index 0000000..3de75de --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/policy/SoftReferenceMapRetryContextCache.java @@ -0,0 +1,104 @@ +/* + * 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.commons.retry.policy; + +import java.lang.ref.SoftReference; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.commons.retry.RetryContext; + +/** + * Map-based implementation of {@link RetryContextCache}. The map backing the + * cache of contexts is synchronized and its entries are soft-referenced, so may + * be garbage collected under pressure. + * + * @see MapRetryContextCache for non-soft referenced version + * + * @author Dave Syer + * + */ +public class SoftReferenceMapRetryContextCache implements RetryContextCache { + + /** + * Default value for maximum capacity of the cache. This is set to a + * reasonably low value (4096) to avoid users inadvertently filling the + * cache with item keys that are inconsistent. + */ + public static final int DEFAULT_CAPACITY = 4096; + + private Map> map = Collections + .synchronizedMap(new HashMap>()); + + private int capacity; + + /** + * Create a {@link SoftReferenceMapRetryContextCache} with default capacity. + */ + public SoftReferenceMapRetryContextCache() { + this(DEFAULT_CAPACITY); + } + + /** + * @param defaultCapacity + */ + public SoftReferenceMapRetryContextCache(int defaultCapacity) { + super(); + this.capacity = defaultCapacity; + } + + /** + * Public setter for the capacity. Prevents the cache from growing + * unboundedly if items that fail are misidentified and two references to an + * identical item actually do not have the same key. This can happen when + * users implement equals and hashCode based on mutable fields, for + * instance. + * + * @param capacity the capacity to set + */ + public void setCapacity(int capacity) { + this.capacity = capacity; + } + + public boolean containsKey(Object key) { + if (!map.containsKey(key)) { + return false; + } + if (map.get(key).get() == null) { + // our reference was garbage collected + map.remove(key); + } + return map.containsKey(key); + } + + public RetryContext get(Object key) { + return map.get(key).get(); + } + + public void put(Object key, RetryContext context) { + if (map.size() >= capacity) { + throw new RetryCacheCapacityExceededException("Retry cache capacity limit breached. " + + "Do you need to re-consider the implementation of the key generator, " + + "or the equals and hashCode of the items that failed?"); + } + map.put(key, new SoftReference(context)); + } + + public void remove(Object key) { + map.remove(key); + } +} diff --git a/src/main/java/org/springframework/commons/retry/policy/TimeoutRetryPolicy.java b/src/main/java/org/springframework/commons/retry/policy/TimeoutRetryPolicy.java new file mode 100644 index 0000000..0d206ed --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/policy/TimeoutRetryPolicy.java @@ -0,0 +1,94 @@ +/* + * 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.commons.retry.policy; + +import org.springframework.commons.retry.RetryContext; +import org.springframework.commons.retry.RetryPolicy; +import org.springframework.commons.retry.context.RetryContextSupport; + +/** + * A {@link RetryPolicy} that allows a retry only if it hasn't timed out. The + * clock is started on a call to {@link #open(RetryContext)}. + * + * @author Dave Syer + * + */ +public class TimeoutRetryPolicy implements RetryPolicy { + + /** + * Default value for timeout (milliseconds). + */ + public static final long DEFAULT_TIMEOUT = 1000; + + private long timeout = DEFAULT_TIMEOUT; + + /** + * Setter for timeout in milliseconds. Default is {@link #DEFAULT_TIMEOUT}. + * @param timeout + */ + public void setTimeout(long timeout) { + this.timeout = timeout; + } + + /** + * The value of the timeout. + * + * @return the timeout in milliseconds + */ + public long getTimeout() { + return timeout; + } + + /** + * Only permits a retry if the timeout has not expired. Does not check the + * exception at all. + * + * @see org.springframework.commons.retry.RetryPolicy#canRetry(org.springframework.commons.retry.RetryContext) + */ + public boolean canRetry(RetryContext context) { + return ((TimeoutRetryContext) context).isAlive(); + } + + public void close(RetryContext context) { + } + + public RetryContext open(RetryContext parent) { + return new TimeoutRetryContext(parent, timeout); + } + + public void registerThrowable(RetryContext context, Throwable throwable) { + ((RetryContextSupport) context).registerThrowable(throwable); + // otherwise no-op - we only time out, otherwise retry everything... + } + + private static class TimeoutRetryContext extends RetryContextSupport { + private long timeout; + + private long start; + + public TimeoutRetryContext(RetryContext parent, long timeout) { + super(parent); + this.start = System.currentTimeMillis(); + this.timeout = timeout; + } + + public boolean isAlive() { + return (System.currentTimeMillis() - start) <= timeout; + } + } + +} diff --git a/src/main/java/org/springframework/commons/retry/support/DefaultRetryState.java b/src/main/java/org/springframework/commons/retry/support/DefaultRetryState.java new file mode 100644 index 0000000..8e8f3bb --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/support/DefaultRetryState.java @@ -0,0 +1,120 @@ +/* + * 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.commons.retry.support; + +import org.springframework.commons.classify.Classifier; +import org.springframework.commons.retry.RecoveryCallback; +import org.springframework.commons.retry.RetryCallback; +import org.springframework.commons.retry.RetryOperations; +import org.springframework.commons.retry.RetryState; + +/** + * + * @author Dave Syer + * + */ +public class DefaultRetryState implements RetryState { + + final private Object key; + + final private boolean forceRefresh; + + final private Classifier rollbackClassifier; + + /** + * Create a {@link DefaultRetryState} representing the state for a new retry + * attempt. + * + * @see RetryOperations#execute(RetryCallback, RetryState) + * @see RetryOperations#execute(RetryCallback, RecoveryCallback, RetryState) + * + * @param key the key for the state to allow this retry attempt to be + * recognised + * @param forceRefresh true if the attempt is known to be a brand new state + * (could not have previously failed) + * @param rollbackClassifier the rollback classifier to set. The rollback + * classifier answers true if the exception provided should cause a + * rollback. + */ + public DefaultRetryState(Object key, boolean forceRefresh, Classifier rollbackClassifier) { + this.key = key; + this.forceRefresh = forceRefresh; + this.rollbackClassifier = rollbackClassifier; + } + + /** + * Defaults the force refresh flag to false. + * @see DefaultRetryState#DefaultRetryState(Object, boolean, Classifier) + */ + public DefaultRetryState(Object key, Classifier rollbackClassifier) { + this(key, false, rollbackClassifier); + } + + /** + * Defaults the rollback classifier to null. + * @see DefaultRetryState#DefaultRetryState(Object, boolean, Classifier) + */ + public DefaultRetryState(Object key, boolean forceRefresh) { + this(key, forceRefresh, null); + } + + /** + * Defaults the force refresh flag (to false) and the rollback classifier + * (to null). + * + * @see DefaultRetryState#DefaultRetryState(Object, boolean, Classifier) + */ + public DefaultRetryState(Object key) { + this(key, false, null); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.commons.retry.IRetryState#getKey() + */ + public Object getKey() { + return key; + } + + /* + * (non-Javadoc) + * + * @see org.springframework.commons.retry.IRetryState#isForceRefresh() + */ + public boolean isForceRefresh() { + return forceRefresh; + } + + /* + * (non-Javadoc) + * + * @see + * org.springframework.commons.retry.RetryState#rollbackFor(java.lang.Throwable + * ) + */ + public boolean rollbackFor(Throwable exception) { + if (rollbackClassifier == null) { + return true; + } + return rollbackClassifier.classify(exception); + } + + @Override + public String toString() { + return String.format("[%s: key=%s, forceRefresh=%b]", getClass().getSimpleName(), key, forceRefresh); + } +} diff --git a/src/main/java/org/springframework/commons/retry/support/RetrySynchronizationManager.java b/src/main/java/org/springframework/commons/retry/support/RetrySynchronizationManager.java new file mode 100644 index 0000000..35c3939 --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/support/RetrySynchronizationManager.java @@ -0,0 +1,80 @@ +/* + * 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.commons.retry.support; + +import org.springframework.commons.repeat.RepeatOperations; +import org.springframework.commons.retry.RetryCallback; +import org.springframework.commons.retry.RetryContext; +import org.springframework.commons.retry.RetryOperations; + +/** + * Global variable support for retry clients. Normally it is not necessary for + * clients to be aware of the surrounding environment because a + * {@link RetryCallback} can always use the context it is passed by the + * enclosing {@link RetryOperations}. But occasionally it might be helpful to + * have lower level access to the ongoing {@link RetryContext} so we provide a + * global accessor here. The mutator methods ({@link #clear()} and + * {@link #register(RetryContext)} should not be used except internally by + * {@link RetryOperations} implementations. + * + * @author Dave Syer + * + */ +public final class RetrySynchronizationManager { + + private RetrySynchronizationManager() {} + + private static final ThreadLocal context = new ThreadLocal(); + + /** + * Public accessor for the locally enclosing {@link RetryContext}. + * + * @return the current retry context, or null if there isn't one + */ + public static RetryContext getContext() { + RetryContext result = (RetryContext) context.get(); + return result; + } + + /** + * Method for registering a context - should only be used by + * {@link RetryOperations} implementations to ensure that + * {@link #getContext()} always returns the correct value. + * + * @param context the new context to register + * @return the old context if there was one + */ + public static RetryContext register(RetryContext context) { + RetryContext oldContext = getContext(); + RetrySynchronizationManager.context.set(context); + return oldContext; + } + + /** + * Clear the current context at the end of a batch - should only be used by + * {@link RepeatOperations} implementations. + * + * @return the old value if there was one. + */ + public static RetryContext clear() { + RetryContext value = getContext(); + RetryContext parent = value == null ? null : value.getParent(); + RetrySynchronizationManager.context.set(parent); + return value; + } + +} diff --git a/src/main/java/org/springframework/commons/retry/support/RetryTemplate.java b/src/main/java/org/springframework/commons/retry/support/RetryTemplate.java new file mode 100644 index 0000000..20a9914 --- /dev/null +++ b/src/main/java/org/springframework/commons/retry/support/RetryTemplate.java @@ -0,0 +1,484 @@ +/* + * 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.commons.retry.support; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.commons.repeat.RepeatException; +import org.springframework.commons.retry.ExhaustedRetryException; +import org.springframework.commons.retry.RecoveryCallback; +import org.springframework.commons.retry.RetryCallback; +import org.springframework.commons.retry.RetryContext; +import org.springframework.commons.retry.RetryException; +import org.springframework.commons.retry.RetryListener; +import org.springframework.commons.retry.RetryOperations; +import org.springframework.commons.retry.RetryPolicy; +import org.springframework.commons.retry.RetryState; +import org.springframework.commons.retry.TerminatedRetryException; +import org.springframework.commons.retry.backoff.BackOffContext; +import org.springframework.commons.retry.backoff.BackOffInterruptedException; +import org.springframework.commons.retry.backoff.BackOffPolicy; +import org.springframework.commons.retry.backoff.NoBackOffPolicy; +import org.springframework.commons.retry.policy.MapRetryContextCache; +import org.springframework.commons.retry.policy.RetryContextCache; +import org.springframework.commons.retry.policy.SimpleRetryPolicy; + +/** + * Template class that simplifies the execution of operations with retry + * semantics.
+ * Retryable operations are encapsulated in implementations of the + * {@link RetryCallback} interface and are executed using one of the supplied + * execute methods.
+ * + * By default, an operation is retried if is throws any {@link Exception} or + * subclass of {@link Exception}. This behaviour can be changed by using the + * {@link #setRetryPolicy(RetryPolicy)} method.
+ * + * Also by default, each operation is retried for a maximum of three attempts + * with no back off in between. This behaviour can be configured using the + * {@link #setRetryPolicy(RetryPolicy)} and + * {@link #setBackOffPolicy(BackOffPolicy)} properties. The + * {@link org.springframework.commons.retry.backoff.BackOffPolicy} controls how + * long the pause is between each individual retry attempt.
+ * + * This class is thread-safe and suitable for concurrent access when executing + * operations and when performing configuration changes. As such, it is possible + * to change the number of retries on the fly, as well as the + * {@link BackOffPolicy} used and no in progress retryable operations will be + * affected. + * + * @author Rob Harrop + * @author Dave Syer + */ +public class RetryTemplate implements RetryOperations { + + protected final Log logger = LogFactory.getLog(getClass()); + + private volatile BackOffPolicy backOffPolicy = new NoBackOffPolicy(); + + private volatile RetryPolicy retryPolicy = new SimpleRetryPolicy(3, Collections + ., Boolean> singletonMap(Exception.class, true)); + + private volatile RetryListener[] listeners = new RetryListener[0]; + + private RetryContextCache retryContextCache = new MapRetryContextCache(); + + /** + * Public setter for the {@link RetryContextCache}. + * + * @param retryContextCache the {@link RetryContextCache} to set. + */ + public void setRetryContextCache(RetryContextCache retryContextCache) { + this.retryContextCache = retryContextCache; + } + + /** + * Setter for listeners. The listeners are executed before and after a retry + * block (i.e. before and after all the attempts), and on an error (every + * attempt). + * + * @param listeners + * @see RetryListener + */ + public void setListeners(RetryListener[] listeners) { + this.listeners = Arrays.asList(listeners).toArray(new RetryListener[listeners.length]); + } + + /** + * Register an additional listener. + * + * @param listener + * @see #setListeners(RetryListener[]) + */ + public void registerListener(RetryListener listener) { + List list = new ArrayList(Arrays.asList(listeners)); + list.add(listener); + listeners = list.toArray(new RetryListener[list.size()]); + } + + /** + * Setter for {@link BackOffPolicy}. + * + * @param backOffPolicy + */ + public void setBackOffPolicy(BackOffPolicy backOffPolicy) { + this.backOffPolicy = backOffPolicy; + } + + /** + * Setter for {@link RetryPolicy}. + * + * @param retryPolicy + */ + public void setRetryPolicy(RetryPolicy retryPolicy) { + this.retryPolicy = retryPolicy; + } + + /** + * Keep executing the callback until it either succeeds or the policy + * dictates that we stop, in which case the most recent exception thrown by + * the callback will be rethrown. + * + * @see RetryOperations#execute(RetryCallback) + * + * @throws TerminatedRetryException if the retry has been manually + * terminated by a listener. + */ + public final T execute(RetryCallback retryCallback) throws Exception { + return doExecute(retryCallback, null, null); + } + + /** + * Keep executing the callback until it either succeeds or the policy + * dictates that we stop, in which case the recovery callback will be + * executed. + * + * @see RetryOperations#execute(RetryCallback, RecoveryCallback) + * + * @throws TerminatedRetryException if the retry has been manually + * terminated by a listener. + */ + public final T execute(RetryCallback retryCallback, RecoveryCallback recoveryCallback) throws Exception { + return doExecute(retryCallback, recoveryCallback, null); + } + + /** + * Execute the callback once if the policy dictates that we can, re-throwing + * any exception encountered so that clients can re-present the same task + * later. + * + * @see RetryOperations#execute(RetryCallback, RetryState) + * + * @throws ExhaustedRetryException if the retry has been exhausted. + */ + public final T execute(RetryCallback retryCallback, RetryState retryState) throws Exception, + ExhaustedRetryException { + return doExecute(retryCallback, null, retryState); + } + + /** + * Execute the callback once if the policy dictates that we can, re-throwing + * any exception encountered so that clients can re-present the same task + * later. + * + * @see RetryOperations#execute(RetryCallback, RetryState) + */ + public final T execute(RetryCallback retryCallback, RecoveryCallback recoveryCallback, + RetryState retryState) throws Exception, ExhaustedRetryException { + return doExecute(retryCallback, recoveryCallback, retryState); + } + + /** + * Execute the callback once if the policy dictates that we can, otherwise + * execute the recovery callback. + * + * @see RetryOperations#execute(RetryCallback, RecoveryCallback, RetryState) + * @throws ExhaustedRetryException if the retry has been exhausted. + */ + protected T doExecute(RetryCallback retryCallback, RecoveryCallback recoveryCallback, RetryState state) + throws Exception, ExhaustedRetryException { + + RetryPolicy retryPolicy = this.retryPolicy; + BackOffPolicy backOffPolicy = this.backOffPolicy; + + // Allow the retry policy to initialise itself... + RetryContext context = open(retryPolicy, state); + logger.debug("RetryContext retrieved: " + context); + + // Make sure the context is available globally for clients who need + // it... + RetrySynchronizationManager.register(context); + + Throwable lastException = null; + + try { + + // Give clients a chance to enhance the context... + boolean running = doOpenInterceptors(retryCallback, context); + + if (!running) { + throw new TerminatedRetryException("Retry terminated abnormally by interceptor before first attempt"); + } + + // Start the backoff context... + BackOffContext backOffContext = backOffPolicy.start(context); + + /* + * We allow the whole loop to be skipped if the policy or context + * already forbid the first try. This is used in the case of + * external retry to allow a recovery in handleRetryExhausted + * without the callback processing (which would throw an exception). + */ + while (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) { + + try { + logger.debug("Retry: count=" + context.getRetryCount()); + // Reset the last exception, so if we are successful + // the close interceptors will not think we failed... + lastException = null; + return retryCallback.doWithRetry(context); + } + catch (Throwable e) { + + lastException = e; + + doOnErrorInterceptors(retryCallback, context, e); + + registerThrowable(retryPolicy, state, context, e); + + if (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) { + try { + backOffPolicy.backOff(backOffContext); + } + catch (BackOffInterruptedException ex) { + lastException = e; + // back off was prevented by another thread - fail + // the retry + logger.debug("Abort retry because interrupted: count=" + context.getRetryCount()); + throw ex; + } + } + + logger.debug("Checking for rethrow: count=" + context.getRetryCount()); + if (shouldRethrow(retryPolicy, context, state)) { + logger.debug("Rethrow in retry for policy: count=" + context.getRetryCount()); + throw wrapIfNecessary(e); + } + + } + + /* + * A stateful attempt that can retry should have rethrown the + * exception by now - i.e. we shouldn't get this far for a + * stateful attempt if it can retry. + */ + } + + logger.debug("Retry failed last attempt: count=" + context.getRetryCount()); + + if (context.isExhaustedOnly()) { + throw new ExhaustedRetryException("Retry exhausted after last attempt with no recovery path.", context + .getLastThrowable()); + } + + return handleRetryExhausted(recoveryCallback, context, state); + + } + finally { + close(retryPolicy, context, state, lastException == null); + doCloseInterceptors(retryCallback, context, lastException); + RetrySynchronizationManager.clear(); + } + + } + + /** + * Decide whether to proceed with the ongoing retry attempt. This method is + * called before the {@link RetryCallback} is executed, but after the + * backoff and open interceptors. + * + * @param retryPolicy the policy to apply + * @param context the current retry context + * @return true if we can continue with the attempt + */ + protected boolean canRetry(RetryPolicy retryPolicy, RetryContext context) { + return retryPolicy.canRetry(context); + } + + /** + * Clean up the cache if necessary and close the context provided (if the + * flag indicates that processing was successful). + * + * @param context + * @param state + * @param succeeded + */ + protected void close(RetryPolicy retryPolicy, RetryContext context, RetryState state, boolean succeeded) { + if (state != null) { + if (succeeded) { + retryContextCache.remove(state.getKey()); + retryPolicy.close(context); + } + } + else { + retryPolicy.close(context); + } + } + + /** + * @param retryPolicy + * @param state + * @param context + * @param e + */ + protected void registerThrowable(RetryPolicy retryPolicy, RetryState state, RetryContext context, Throwable e) { + if (state != null) { + Object key = state.getKey(); + if (context.getRetryCount() > 0 && !retryContextCache.containsKey(key)) { + throw new RetryException("Inconsistent state for failed item key: cache key has changed. " + + "Consider whether equals() or hashCode() for the key might be inconsistent, " + + "or if you need to supply a better key"); + } + retryContextCache.put(key, context); + } + retryPolicy.registerThrowable(context, e); + } + + /** + * Delegate to the {@link RetryPolicy} having checked in the cache for an + * existing value if the state is not null. + * + * @param retryPolicy a {@link RetryPolicy} to delegate the context creation + * @return a retry context, either a new one or the one used last time the + * same state was encountered + */ + protected RetryContext open(RetryPolicy retryPolicy, RetryState state) { + + if (state == null) { + return doOpenInternal(retryPolicy); + } + + Object key = state.getKey(); + if (state.isForceRefresh()) { + return doOpenInternal(retryPolicy); + } + + // If there is no cache hit we can avoid the possible expense of the + // cache re-hydration. + if (!retryContextCache.containsKey(key)) { + // The cache is only used if there is a failure. + return doOpenInternal(retryPolicy); + } + + RetryContext context = retryContextCache.get(key); + if (context == null) { + if (retryContextCache.containsKey(key)) { + throw new RetryException("Inconsistent state for failed item: no history found. " + + "Consider whether equals() or hashCode() for the item might be inconsistent, " + + "or if you need to supply a better ItemKeyGenerator"); + } + // The cache could have been expired in between calls to + // containsKey(), so we have to live with this: + return doOpenInternal(retryPolicy); + } + + return context; + + } + + /** + * @param retryPolicy + * @return + */ + private RetryContext doOpenInternal(RetryPolicy retryPolicy) { + return retryPolicy.open(RetrySynchronizationManager.getContext()); + } + + /** + * Actions to take after final attempt has failed. If there is state clean + * up the cache. If there is a recovery callback, execute that and return + * its result. Otherwise throw an exception. + * + * @param recoveryCallback the callback for recovery (might be null) + * @param context the current retry context + * @throws Exception if the callback does, and if there is no callback and + * the state is null then the last exception from the context + * @throws ExhaustedRetryException if the state is not null and there is no + * recovery callback + */ + protected T handleRetryExhausted(RecoveryCallback recoveryCallback, RetryContext context, RetryState state) + throws Exception { + if (state != null) { + retryContextCache.remove(state.getKey()); + } + if (recoveryCallback != null) { + return recoveryCallback.recover(context); + } + if (state != null) { + logger.debug("Retry exhausted after last attempt with no recovery path."); + throw new ExhaustedRetryException("Retry exhausted after last attempt with no recovery path", context + .getLastThrowable()); + } + throw wrapIfNecessary(context.getLastThrowable()); + } + + /** + * Extension point for subclasses to decide on behaviour after catching an + * exception in a {@link RetryCallback}. Normal stateless behaviour is not + * to rethrow, and if there is state we rethrow. + * + * @param retryPolicy + * @param context the current context + * + * @return true if the state is not null but subclasses might choose + * otherwise + */ + protected boolean shouldRethrow(RetryPolicy retryPolicy, RetryContext context, RetryState state) { + if (state == null) { + return false; + } + else { + return state.rollbackFor(context.getLastThrowable()); + } + } + + private boolean doOpenInterceptors(RetryCallback callback, RetryContext context) { + + boolean result = true; + + for (int i = 0; i < listeners.length; i++) { + result = result && listeners[i].open(context, callback); + } + + return result; + + } + + private void doCloseInterceptors(RetryCallback callback, RetryContext context, Throwable lastException) { + for (int i = listeners.length; i-- > 0;) { + listeners[i].close(context, callback, lastException); + } + } + + private void doOnErrorInterceptors(RetryCallback callback, RetryContext context, Throwable throwable) { + for (int i = listeners.length; i-- > 0;) { + listeners[i].onError(context, callback, throwable); + } + } + + /** + * Re-throws the original throwable if it is unchecked, wraps checked + * exceptions into {@link RepeatException}. + */ + private static Exception wrapIfNecessary(Throwable throwable) { + if (throwable instanceof Error) { + throw (Error) throwable; + } + else if (throwable instanceof Exception) { + return (Exception) throwable; + } + else { + return new RetryException("Exception in batch process", throwable); + } + } + +} diff --git a/src/site/site.xml b/src/site/site.xml new file mode 100644 index 0000000..4bba09c --- /dev/null +++ b/src/site/site.xml @@ -0,0 +1,25 @@ + + + + + Spring Batch: ${project.name} + index.html + + + + org.springframework.maven.skins + maven-spring-skin + 1.0.5 + + + + + + + + + + + + + diff --git a/src/test/resources/log4j.properties b/src/test/resources/log4j.properties new file mode 100644 index 0000000..000f089 --- /dev/null +++ b/src/test/resources/log4j.properties @@ -0,0 +1,7 @@ +log4j.rootCategory=INFO, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +log4j.category.org.springframework.transaction=INFO diff --git a/template.mf b/template.mf new file mode 100644 index 0000000..6ffa42e --- /dev/null +++ b/template.mf @@ -0,0 +1,18 @@ +Bundle-SymbolicName: org.springframework.commons.retry +Bundle-Name: Spring Commons Retry +Bundle-Vendor: SpringSource +Bundle-ManifestVersion: 2 +Import-Template: + org.springframework.commons.*;version="[1.0.0, 2.0.0)", + org.springframework.beans.*;version="[3.0.0, 4.0.0)", + org.springframework.context.*;version="[3.0.0, 4.0.0)", + org.springframework.core.*;version="[3.0.0, 4.0.0)", + org.springframework.jdbc.*;version="[3.0.0, 4.0.0)", + org.springframework.stereotype.*;version="[3.0.0, 4.0.0)", + org.springframework.scheduling.*;version="[3.0.0, 4.0.0)", + org.springframework.ui.*;version="[3.0.0, 4.0.0)", + org.springframework.transaction.*;version="[3.0.0, 4.0.0)", + org.springframework.web.*;version="[3.0.0, 4.0.0)", + org.springframework.validation.*;version="[3.0.0, 4.0.0)", + org.springframework.util;version="[3.0.0, 4.0.0)", + org.apache.commons.logging;version="[1.1.1, 2.0.0)"