BATCH-804: add interface for RetryState
This commit is contained in:
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.batch.retry;
|
||||
|
||||
import org.springframework.batch.retry.support.DefaultRetryState;
|
||||
|
||||
/**
|
||||
* Defines the basic set of operations implemented by {@link RetryOperations} to
|
||||
* execute operations with configurable retry behaviour.
|
||||
@@ -50,7 +52,7 @@ public interface RetryOperations {
|
||||
|
||||
/**
|
||||
* A simple stateful retry. Execute the supplied {@link RetryCallback} with
|
||||
* a target object for the attempt identified by the {@link RetryState}.
|
||||
* 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
|
||||
@@ -72,7 +74,7 @@ public interface RetryOperations {
|
||||
* 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 RetryState}.
|
||||
* identified by the {@link DefaultRetryState}.
|
||||
*
|
||||
* @see #execute(RetryCallback, RetryState)
|
||||
*
|
||||
|
||||
@@ -16,38 +16,45 @@
|
||||
package org.springframework.batch.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 class RetryState {
|
||||
|
||||
final private Object key;
|
||||
final private boolean forceRefresh;
|
||||
public interface RetryState {
|
||||
|
||||
/**
|
||||
* @param key
|
||||
* @param forceRefresh
|
||||
*/
|
||||
public RetryState(Object key, boolean forceRefresh) {
|
||||
this.key = key;
|
||||
this.forceRefresh = forceRefresh;
|
||||
}
|
||||
|
||||
public RetryState(Object key) {
|
||||
this(key, false);
|
||||
}
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public Object getKey() {
|
||||
return key;
|
||||
}
|
||||
Object getKey();
|
||||
|
||||
/**
|
||||
* @return true if the state requires an explicit check for the key
|
||||
* 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
|
||||
*/
|
||||
public boolean isForceRefresh() {
|
||||
return forceRefresh;
|
||||
}
|
||||
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(Exception exception);
|
||||
|
||||
}
|
||||
@@ -21,13 +21,14 @@ import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.batch.retry.ExhaustedRetryException;
|
||||
import org.springframework.batch.retry.RetryState;
|
||||
import org.springframework.batch.retry.RecoveryCallback;
|
||||
import org.springframework.batch.retry.RetryCallback;
|
||||
import org.springframework.batch.retry.RetryContext;
|
||||
import org.springframework.batch.retry.RetryOperations;
|
||||
import org.springframework.batch.retry.RetryPolicy;
|
||||
import org.springframework.batch.retry.RetryState;
|
||||
import org.springframework.batch.retry.policy.NeverRetryPolicy;
|
||||
import org.springframework.batch.retry.support.DefaultRetryState;
|
||||
import org.springframework.batch.retry.support.RetryTemplate;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
@@ -138,7 +139,7 @@ public class StatefulRetryOperationsInterceptor implements MethodInterceptor {
|
||||
}
|
||||
final Object item = arg;
|
||||
|
||||
RetryState retryState = new RetryState(keyGenerator != null ? keyGenerator.getKey(args) : item, newMethodArgumentsIdentifier != null ? newMethodArgumentsIdentifier.isNew(args) : false );
|
||||
RetryState retryState = new DefaultRetryState(keyGenerator != null ? keyGenerator.getKey(args) : item, newMethodArgumentsIdentifier != null ? newMethodArgumentsIdentifier.isNew(args) : false );
|
||||
|
||||
Object result = retryTemplate.execute(new MethodInvocationRetryCallback(invocation), new ItemRecovererCallback(args, recoverer), retryState);
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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.batch.retry.support;
|
||||
|
||||
import org.springframework.batch.retry.RecoveryCallback;
|
||||
import org.springframework.batch.retry.RetryCallback;
|
||||
import org.springframework.batch.retry.RetryOperations;
|
||||
import org.springframework.batch.retry.RetryState;
|
||||
import org.springframework.batch.support.Classifier;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
public class DefaultRetryState implements RetryState {
|
||||
|
||||
final private Object key;
|
||||
|
||||
final private boolean forceRefresh;
|
||||
|
||||
final private Classifier<? super Throwable, Boolean> 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
|
||||
* @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<? super Throwable, Boolean> rollbackClassifier) {
|
||||
this.key = key;
|
||||
this.forceRefresh = forceRefresh;
|
||||
this.rollbackClassifier = rollbackClassifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defaults the rollback classifier to null.
|
||||
* @see DefaultRetryState#DefaultRetryState(Object, boolean, Classifier)
|
||||
*/
|
||||
public DefaultRetryState(Object key, Classifier<? super Throwable, Boolean> 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.batch.retry.IRetryState#getKey()
|
||||
*/
|
||||
public Object getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.batch.retry.IRetryState#isForceRefresh()
|
||||
*/
|
||||
public boolean isForceRefresh() {
|
||||
return forceRefresh;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.batch.retry.IRetryState#rollbackFor(java.lang.Exception)
|
||||
*/
|
||||
public boolean rollbackFor(Exception exception) {
|
||||
if (rollbackClassifier == null) {
|
||||
return true;
|
||||
}
|
||||
return rollbackClassifier.classify(exception);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import java.util.List;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.batch.retry.ExhaustedRetryException;
|
||||
import org.springframework.batch.retry.RetryState;
|
||||
import org.springframework.batch.retry.RecoveryCallback;
|
||||
import org.springframework.batch.retry.RetryCallback;
|
||||
import org.springframework.batch.retry.RetryContext;
|
||||
@@ -30,7 +31,6 @@ import org.springframework.batch.retry.RetryException;
|
||||
import org.springframework.batch.retry.RetryListener;
|
||||
import org.springframework.batch.retry.RetryOperations;
|
||||
import org.springframework.batch.retry.RetryPolicy;
|
||||
import org.springframework.batch.retry.RetryState;
|
||||
import org.springframework.batch.retry.TerminatedRetryException;
|
||||
import org.springframework.batch.retry.backoff.BackOffContext;
|
||||
import org.springframework.batch.retry.backoff.BackOffInterruptedException;
|
||||
@@ -39,7 +39,6 @@ import org.springframework.batch.retry.backoff.NoBackOffPolicy;
|
||||
import org.springframework.batch.retry.policy.MapRetryContextCache;
|
||||
import org.springframework.batch.retry.policy.RetryContextCache;
|
||||
import org.springframework.batch.retry.policy.SimpleRetryPolicy;
|
||||
import org.springframework.batch.support.Classifier;
|
||||
|
||||
/**
|
||||
* Template class that simplifies the execution of operations with retry
|
||||
@@ -79,45 +78,6 @@ public class RetryTemplate implements RetryOperations {
|
||||
|
||||
private RetryContextCache retryContextCache = new MapRetryContextCache();
|
||||
|
||||
private Classifier<? super Throwable, Boolean> rollbackClassifier = null;
|
||||
|
||||
/**
|
||||
* Public setter for the rollback classifier. This classifier answers its
|
||||
* default if the exception provided should not cause a rollback. I.e.
|
||||
* anything other than the default will lead to a rollback.<br/><br/>
|
||||
*
|
||||
* The decision whether to rollback or not is unrelated to that of the
|
||||
* {@link RetryPolicy}, but the policy can be accidentally inconsistent
|
||||
* with the rollback decision. E.g. in a stateless retry the policy might be
|
||||
* configured to allow retry on a rollback, but that wouldn't make sense - a
|
||||
* stateful retry should have been used. The best we can do in such
|
||||
* circumstances is throw a {@link RetryException} from
|
||||
* {@link #execute(RetryCallback)} or
|
||||
* {@link #execute(RetryCallback, RecoveryCallback)}. The recovery path
|
||||
* will not be taken in such situations.<br/><br/>
|
||||
*
|
||||
* For stateless retry it is often adequate to use the default behaviour, as
|
||||
* long as one is careful with the retry policy (exceptions which should
|
||||
* cause rollback are still not really retryable in a transactional
|
||||
* setting).<br/><br/>
|
||||
*
|
||||
* For stateful retry adding a classifier will allow an optimisation:
|
||||
* exceptions which are not marked for rollback can still be retried, but
|
||||
* without paying the cost of a rollback. Effectively one is overriding the
|
||||
* stateful quality of the retry dynamically, according to the exception
|
||||
* type.<br/><br/>
|
||||
*
|
||||
* Example usage would be for a stateful retry to specify a validation exception as not for rollback
|
||||
*
|
||||
* If not set then the default is to rollback for all exceptions when the
|
||||
* retry is stateful, and for none when it is stateless.
|
||||
*
|
||||
* @param rollbackClassifier the rollback classifier to set
|
||||
*/
|
||||
public void setRollbackClassifier(Classifier<? super Throwable, Boolean> rollbackClassifier) {
|
||||
this.rollbackClassifier = rollbackClassifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public setter for the {@link RetryContextCache}.
|
||||
* @param retryContextCache the {@link RetryContextCache} to set.
|
||||
@@ -446,20 +406,12 @@ public class RetryTemplate implements RetryOperations {
|
||||
* otherwise
|
||||
*/
|
||||
protected boolean shouldRethrow(RetryPolicy retryPolicy, RetryContext context, RetryState state) {
|
||||
// Allow stateless behaviour to take over for certain exception types
|
||||
if (rollbackClassifier != null) {
|
||||
// TODO: remove this. Make it part of the stateful execution parameters?
|
||||
// Then we wouldn't have to make assertions about the stateless case.
|
||||
boolean rollback = rollbackClassifier.classify(context.getLastThrowable());
|
||||
if (rollback && state == null && retryPolicy.canRetry(context)) {
|
||||
throw new RetryException("Inconsistent configuration. The retry policy says we can retry but "
|
||||
+ "the exception has been marked for rollback.", context.getLastThrowable());
|
||||
}
|
||||
return rollback;
|
||||
if (state == null) {
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
return state.rollbackFor(context.getLastThrowable());
|
||||
}
|
||||
// If no classifier is provided, just assume the all exceptions are for
|
||||
// rollback if the execution is stateful, and none otherwise.
|
||||
return state != null;
|
||||
}
|
||||
|
||||
private <T> boolean doOpenInterceptors(RetryCallback<T> callback, RetryContext context) {
|
||||
|
||||
Reference in New Issue
Block a user