OPEN - issue BATCH-777: Parametrise RetryCallback and related interfaces
Make stateful retry explicit in the RepeatOperations interface instead of hidden in a RetryPolicy
This commit is contained in:
@@ -13,7 +13,6 @@ import org.springframework.batch.core.step.skip.ItemSkipPolicy;
|
||||
import org.springframework.batch.core.step.skip.LimitCheckingItemSkipPolicy;
|
||||
import org.springframework.batch.core.step.skip.SkipLimitExceededException;
|
||||
import org.springframework.batch.core.step.skip.SkipListenerFailedException;
|
||||
import org.springframework.batch.item.ItemKeyGenerator;
|
||||
import org.springframework.batch.item.ItemProcessor;
|
||||
import org.springframework.batch.item.ItemReader;
|
||||
import org.springframework.batch.item.ItemWriter;
|
||||
@@ -26,12 +25,11 @@ 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.backoff.BackOffPolicy;
|
||||
import org.springframework.batch.retry.callback.RecoveryRetryCallback;
|
||||
import org.springframework.batch.retry.policy.ExceptionClassifierRetryPolicy;
|
||||
import org.springframework.batch.retry.policy.MapRetryContextCache;
|
||||
import org.springframework.batch.retry.policy.NeverRetryPolicy;
|
||||
import org.springframework.batch.retry.policy.RecoveryCallbackRetryPolicy;
|
||||
import org.springframework.batch.retry.policy.RetryContextCache;
|
||||
import org.springframework.batch.retry.policy.SimpleRetryPolicy;
|
||||
import org.springframework.batch.retry.support.RetryTemplate;
|
||||
@@ -69,8 +67,6 @@ public class SkipLimitStepFactoryBean<T, S> extends SimpleStepFactoryBean<T, S>
|
||||
skippableExceptionClasses.add(Exception.class);
|
||||
}
|
||||
|
||||
private ItemKeyGenerator itemKeyGenerator;
|
||||
|
||||
private int cacheCapacity = 0;
|
||||
|
||||
private int retryLimit = 0;
|
||||
@@ -193,17 +189,6 @@ public class SkipLimitStepFactoryBean<T, S> extends SimpleStepFactoryBean<T, S>
|
||||
this.fatalExceptionClasses = fatalExceptionClasses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public setter for the {@link ItemKeyGenerator}. This is used to identify
|
||||
* failed items so they can be skipped if encountered again, generally in
|
||||
* another transaction.
|
||||
*
|
||||
* @param itemKeyGenerator the {@link ItemKeyGenerator} to set.
|
||||
*/
|
||||
public void setItemKeyGenerator(ItemKeyGenerator itemKeyGenerator) {
|
||||
this.itemKeyGenerator = itemKeyGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the {@link #setSkipLimit(int)} value to configure item handler and
|
||||
* and exception handler.
|
||||
@@ -242,6 +227,11 @@ public class SkipLimitStepFactoryBean<T, S> extends SimpleStepFactoryBean<T, S>
|
||||
retryPolicy = classifierRetryPolicy;
|
||||
|
||||
}
|
||||
RetryTemplate retryTemplate = new RetryTemplate();
|
||||
if (backOffPolicy != null) {
|
||||
retryTemplate.setBackOffPolicy(backOffPolicy);
|
||||
}
|
||||
retryTemplate.setRetryPolicy(retryPolicy);
|
||||
|
||||
// Co-ordinate the retry policy with the exception handler:
|
||||
RepeatOperations stepOperations = getStepOperations();
|
||||
@@ -250,29 +240,18 @@ public class SkipLimitStepFactoryBean<T, S> extends SimpleStepFactoryBean<T, S>
|
||||
getExceptionHandler(), fatalExceptionClasses));
|
||||
}
|
||||
|
||||
RecoveryCallbackRetryPolicy recoveryCallbackRetryPolicy = new RecoveryCallbackRetryPolicy(retryPolicy) {
|
||||
protected boolean recoverForException(Throwable ex) {
|
||||
return !getTransactionAttribute().rollbackOn(ex);
|
||||
}
|
||||
};
|
||||
|
||||
if (retryContextCache == null) {
|
||||
if (cacheCapacity > 0) {
|
||||
recoveryCallbackRetryPolicy.setRetryContextCache(new MapRetryContextCache(cacheCapacity));
|
||||
retryTemplate.setRetryContextCache(new MapRetryContextCache(cacheCapacity));
|
||||
}
|
||||
}
|
||||
else {
|
||||
recoveryCallbackRetryPolicy.setRetryContextCache(retryContextCache);
|
||||
retryTemplate.setRetryContextCache(retryContextCache);
|
||||
}
|
||||
|
||||
RetryTemplate retryTemplate = new RetryTemplate();
|
||||
if (retryListeners != null) {
|
||||
retryTemplate.setListeners(retryListeners);
|
||||
}
|
||||
retryTemplate.setRetryPolicy(recoveryCallbackRetryPolicy);
|
||||
if (retryPolicy == null && backOffPolicy != null) {
|
||||
retryTemplate.setBackOffPolicy(backOffPolicy);
|
||||
}
|
||||
|
||||
List<Class<? extends Throwable>> exceptions = new ArrayList<Class<? extends Throwable>>(
|
||||
skippableExceptionClasses);
|
||||
@@ -282,8 +261,8 @@ public class SkipLimitStepFactoryBean<T, S> extends SimpleStepFactoryBean<T, S>
|
||||
ItemSkipPolicy writeSkipPolicy = new LimitCheckingItemSkipPolicy(skipLimit, exceptions,
|
||||
new ArrayList<Class<? extends Throwable>>(fatalExceptionClasses));
|
||||
StatefulRetryStepHandler<T, S> itemHandler = new StatefulRetryStepHandler<T, S>(getItemReader(),
|
||||
getItemProcessor(), getItemWriter(), getChunkOperations(), retryTemplate, itemKeyGenerator,
|
||||
readSkipPolicy, writeSkipPolicy);
|
||||
getItemProcessor(), getItemWriter(), getChunkOperations(), retryTemplate, readSkipPolicy,
|
||||
writeSkipPolicy);
|
||||
itemHandler.setSkipListeners(BatchListenerFactoryHelper.getSkipListeners(getListeners()));
|
||||
|
||||
step.setStepHandler(itemHandler);
|
||||
@@ -320,8 +299,6 @@ public class SkipLimitStepFactoryBean<T, S> extends SimpleStepFactoryBean<T, S>
|
||||
|
||||
final private RetryOperations retryOperations;
|
||||
|
||||
final private ItemKeyGenerator itemKeyGenerator;
|
||||
|
||||
final private CompositeSkipListener listener = new CompositeSkipListener();
|
||||
|
||||
final private ItemSkipPolicy readSkipPolicy;
|
||||
@@ -332,15 +309,13 @@ public class SkipLimitStepFactoryBean<T, S> extends SimpleStepFactoryBean<T, S>
|
||||
* @param itemReader
|
||||
* @param itemWriter
|
||||
* @param retryTemplate
|
||||
* @param itemKeyGenerator
|
||||
*/
|
||||
public StatefulRetryStepHandler(ItemReader<? extends T> itemReader,
|
||||
ItemProcessor<? super T, ? extends S> itemProcessor, ItemWriter<? super S> itemWriter,
|
||||
RepeatOperations chunkOperations, RetryOperations retryTemplate, ItemKeyGenerator itemKeyGenerator,
|
||||
ItemSkipPolicy readSkipPolicy, ItemSkipPolicy writeSkipPolicy) {
|
||||
RepeatOperations chunkOperations, RetryOperations retryTemplate, ItemSkipPolicy readSkipPolicy,
|
||||
ItemSkipPolicy writeSkipPolicy) {
|
||||
super(itemReader, itemProcessor, itemWriter, chunkOperations);
|
||||
this.retryOperations = retryTemplate;
|
||||
this.itemKeyGenerator = itemKeyGenerator;
|
||||
this.readSkipPolicy = readSkipPolicy;
|
||||
this.writeSkipPolicy = writeSkipPolicy;
|
||||
}
|
||||
@@ -457,14 +432,16 @@ public class SkipLimitStepFactoryBean<T, S> extends SimpleStepFactoryBean<T, S>
|
||||
*/
|
||||
private void retryChunk(final Chunk<S> chunk, final StepContribution contribution) throws Exception {
|
||||
|
||||
RecoveryRetryCallback retryCallback = new RecoveryRetryCallback(chunk, new RetryCallback() {
|
||||
RetryCallback retryCallback = new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
doWrite(chunk.getItems());
|
||||
// TODO: if there is an exception marked as no rollback it
|
||||
// should get treated as stateless
|
||||
return null;
|
||||
}
|
||||
}, chunk);
|
||||
};
|
||||
|
||||
retryCallback.setRecoveryCallback(new RecoveryCallback() {
|
||||
RecoveryCallback recoveryCallback = new RecoveryCallback() {
|
||||
|
||||
public Object recover(RetryContext context) throws Exception {
|
||||
|
||||
@@ -490,10 +467,10 @@ public class SkipLimitStepFactoryBean<T, S> extends SimpleStepFactoryBean<T, S>
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
retryOperations.execute(retryCallback);
|
||||
retryOperations.execute(retryCallback, recoveryCallback, new RetryState(chunk));
|
||||
}
|
||||
catch (Exception e) {
|
||||
// only if the retry failed do we re-arrange the chunk
|
||||
|
||||
@@ -30,9 +30,8 @@ import org.junit.runner.RunWith;
|
||||
import org.springframework.batch.retry.RecoveryCallback;
|
||||
import org.springframework.batch.retry.RetryCallback;
|
||||
import org.springframework.batch.retry.RetryContext;
|
||||
import org.springframework.batch.retry.callback.RecoveryRetryCallback;
|
||||
import org.springframework.batch.retry.RetryState;
|
||||
import org.springframework.batch.retry.policy.NeverRetryPolicy;
|
||||
import org.springframework.batch.retry.policy.RecoveryCallbackRetryPolicy;
|
||||
import org.springframework.batch.retry.support.RetryTemplate;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jms.core.JmsTemplate;
|
||||
@@ -119,23 +118,23 @@ public class BatchMessageListenerContainerIntegrationTests {
|
||||
@Test
|
||||
public void testFailureAndRecovery() throws Exception {
|
||||
final RetryTemplate retryTemplate = new RetryTemplate();
|
||||
retryTemplate.setRetryPolicy(new RecoveryCallbackRetryPolicy(new NeverRetryPolicy()));
|
||||
retryTemplate.setRetryPolicy(new NeverRetryPolicy());
|
||||
container.setMessageListener(new MessageListener() {
|
||||
public void onMessage(final Message msg) {
|
||||
try {
|
||||
RecoveryRetryCallback callback = new RecoveryRetryCallback(msg, new RetryCallback() {
|
||||
RetryCallback callback = new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
count++;
|
||||
throw new RuntimeException("planned failure: " + msg);
|
||||
}
|
||||
}, msg.getJMSMessageID());
|
||||
callback.setRecoveryCallback(new RecoveryCallback() {
|
||||
};
|
||||
RecoveryCallback recoveryCallback = new RecoveryCallback() {
|
||||
public Object recover(RetryContext context) {
|
||||
recovered++;
|
||||
return msg;
|
||||
}
|
||||
});
|
||||
retryTemplate.execute(callback);
|
||||
};
|
||||
retryTemplate.execute(callback, recoveryCallback, new RetryState(msg.getJMSMessageID()));
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw (RuntimeException) e;
|
||||
|
||||
@@ -38,8 +38,7 @@ import org.springframework.batch.repeat.support.RepeatTemplate;
|
||||
import org.springframework.batch.retry.RecoveryCallback;
|
||||
import org.springframework.batch.retry.RetryCallback;
|
||||
import org.springframework.batch.retry.RetryContext;
|
||||
import org.springframework.batch.retry.callback.RecoveryRetryCallback;
|
||||
import org.springframework.batch.retry.policy.RecoveryCallbackRetryPolicy;
|
||||
import org.springframework.batch.retry.RetryState;
|
||||
import org.springframework.batch.retry.policy.SimpleRetryPolicy;
|
||||
import org.springframework.batch.retry.support.RetryTemplate;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -116,7 +115,7 @@ public class ExternalRetryInBatchTests {
|
||||
public void testExternalRetryRecoveryInBatch() throws Exception {
|
||||
assertInitialState();
|
||||
|
||||
retryTemplate.setRetryPolicy(new RecoveryCallbackRetryPolicy(new SimpleRetryPolicy(1)));
|
||||
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(1));
|
||||
|
||||
repeatTemplate.setCompletionPolicy(new SimpleCompletionPolicy(2));
|
||||
|
||||
@@ -138,7 +137,7 @@ public class ExternalRetryInBatchTests {
|
||||
return ExitStatus.FINISHED;
|
||||
}
|
||||
|
||||
RecoveryRetryCallback callback = new RecoveryRetryCallback(item, new RetryCallback() {
|
||||
RetryCallback callback = new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
// No need for transaction here: the whole batch will roll
|
||||
// back. When it comes back for recovery this code is not
|
||||
@@ -148,17 +147,17 @@ public class ExternalRetryInBatchTests {
|
||||
list.size(), item);
|
||||
throw new RuntimeException("Rollback!");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
callback.setRecoveryCallback(new RecoveryCallback() {
|
||||
RecoveryCallback recoveryCallback = new RecoveryCallback() {
|
||||
public Object recover(RetryContext context) {
|
||||
// aggressive commit on a recovery
|
||||
RepeatSynchronizationManager.setCompleteOnly();
|
||||
return provider.recover(item, context.getLastThrowable());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
retryTemplate.execute(callback);
|
||||
retryTemplate.execute(callback, recoveryCallback, new RetryState(item));
|
||||
|
||||
return ExitStatus.CONTINUABLE;
|
||||
|
||||
|
||||
@@ -34,8 +34,7 @@ import org.springframework.batch.item.ItemWriter;
|
||||
import org.springframework.batch.retry.RecoveryCallback;
|
||||
import org.springframework.batch.retry.RetryCallback;
|
||||
import org.springframework.batch.retry.RetryContext;
|
||||
import org.springframework.batch.retry.callback.RecoveryRetryCallback;
|
||||
import org.springframework.batch.retry.policy.RecoveryCallbackRetryPolicy;
|
||||
import org.springframework.batch.retry.RetryState;
|
||||
import org.springframework.batch.retry.support.RetryTemplate;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
|
||||
@@ -106,8 +105,6 @@ public class ExternalRetryTests {
|
||||
|
||||
assertInitialState();
|
||||
|
||||
retryTemplate.setRetryPolicy(new RecoveryCallbackRetryPolicy());
|
||||
|
||||
final ItemWriter<Object> writer = new ItemWriter<Object>() {
|
||||
public void write(final List<? extends Object> texts) {
|
||||
|
||||
@@ -129,13 +126,13 @@ public class ExternalRetryTests {
|
||||
public Object doInTransaction(TransactionStatus status) {
|
||||
try {
|
||||
final Object item = provider.read();
|
||||
RecoveryRetryCallback callback = new RecoveryRetryCallback(item, new RetryCallback() {
|
||||
RetryCallback callback = new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
writer.write(Collections.singletonList(item));
|
||||
return null;
|
||||
}
|
||||
});
|
||||
return retryTemplate.execute(callback);
|
||||
};
|
||||
return retryTemplate.execute(callback, new RetryState(item));
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
@@ -157,13 +154,13 @@ public class ExternalRetryTests {
|
||||
public Object doInTransaction(TransactionStatus status) {
|
||||
try {
|
||||
final Object item = provider.read();
|
||||
RecoveryRetryCallback callback = new RecoveryRetryCallback(item, new RetryCallback() {
|
||||
RetryCallback callback = new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
writer.write(Collections.singletonList(item));
|
||||
return null;
|
||||
}
|
||||
});
|
||||
return retryTemplate.execute(callback);
|
||||
};
|
||||
return retryTemplate.execute(callback, new RetryState(item));
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
@@ -189,21 +186,19 @@ public class ExternalRetryTests {
|
||||
|
||||
assertInitialState();
|
||||
|
||||
retryTemplate.setRetryPolicy(new RecoveryCallbackRetryPolicy());
|
||||
|
||||
final Object item = provider.read();
|
||||
final RecoveryRetryCallback callback = new RecoveryRetryCallback(item, new RetryCallback() {
|
||||
final RetryCallback callback = new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
simpleJdbcTemplate.update("INSERT into T_FOOS (id,name,foo_date) values (?,?,null)", list.size(), item);
|
||||
throw new RuntimeException("Rollback!");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
callback.setRecoveryCallback(new RecoveryCallback() {
|
||||
final RecoveryCallback recoveryCallback = new RecoveryCallback() {
|
||||
public Object recover(RetryContext context) {
|
||||
return provider.recover(item, context.getLastThrowable());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Object result = "start";
|
||||
|
||||
@@ -212,7 +207,7 @@ public class ExternalRetryTests {
|
||||
result = new TransactionTemplate(transactionManager).execute(new TransactionCallback() {
|
||||
public Object doInTransaction(TransactionStatus status) {
|
||||
try {
|
||||
return retryTemplate.execute(callback);
|
||||
return retryTemplate.execute(callback, recoveryCallback, new RetryState(item));
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
|
||||
@@ -16,35 +16,38 @@
|
||||
|
||||
package org.springframework.batch.retry.jms;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.batch.item.jms.JmsItemReader;
|
||||
import org.springframework.batch.jms.ExternalRetryInBatchTests;
|
||||
import org.springframework.batch.retry.RetryCallback;
|
||||
import org.springframework.batch.retry.RetryContext;
|
||||
import org.springframework.batch.retry.callback.RecoveryRetryCallback;
|
||||
import org.springframework.batch.retry.RetryState;
|
||||
import org.springframework.batch.retry.support.RetryTemplate;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
|
||||
import org.springframework.jms.core.JmsTemplate;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.transaction.BeforeTransaction;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.context.transaction.AfterTransaction;
|
||||
import org.springframework.transaction.TransactionStatus;
|
||||
import org.springframework.test.context.transaction.BeforeTransaction;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.TransactionStatus;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.TransactionCallback;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@ContextConfiguration(locations = "/org/springframework/batch/jms/jms-context.xml")
|
||||
@@ -172,7 +175,7 @@ public class SynchronousTests {
|
||||
|
||||
final Object item = provider.read();
|
||||
|
||||
retryTemplate.execute(new RecoveryRetryCallback(item, new RetryCallback() {
|
||||
retryTemplate.execute(new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
|
||||
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
|
||||
@@ -193,7 +196,7 @@ public class SynchronousTests {
|
||||
});
|
||||
|
||||
}
|
||||
}));
|
||||
}, new RetryState(item));
|
||||
|
||||
// Verify the state after stransactional processing is complete
|
||||
|
||||
|
||||
@@ -38,7 +38,8 @@ public interface RetryOperations {
|
||||
|
||||
/**
|
||||
* Execute the supplied {@link RetryCallback} with a fallback on exhausted
|
||||
* retry to the {@link RecoveryCallback}. See implementations for configuration details.
|
||||
* 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.
|
||||
@@ -47,4 +48,40 @@ public interface RetryOperations {
|
||||
*/
|
||||
Object 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 RetryState}.
|
||||
* 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.<br/><br/>
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
Object 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 RetryState}.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
Object execute(RetryCallback retryCallback, RecoveryCallback recoveryCallback, RetryState retryState)
|
||||
throws Exception;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
public class RetryState {
|
||||
|
||||
final private Object key;
|
||||
final private boolean forceRefresh;
|
||||
|
||||
/**
|
||||
* @param key
|
||||
* @param forceRefresh
|
||||
*/
|
||||
public RetryState(Object key, boolean forceRefresh) {
|
||||
this.key = key;
|
||||
this.forceRefresh = forceRefresh;
|
||||
}
|
||||
|
||||
public RetryState(Object key) {
|
||||
this(key, false);
|
||||
}
|
||||
/**
|
||||
* @return the key that this state represents
|
||||
*/
|
||||
public Object getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the state requires an explicit check for the key
|
||||
*/
|
||||
public boolean isForceRefresh() {
|
||||
return forceRefresh;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
/*
|
||||
* 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.callback;
|
||||
|
||||
import org.springframework.batch.retry.RecoveryCallback;
|
||||
import org.springframework.batch.retry.RetryCallback;
|
||||
import org.springframework.batch.retry.RetryContext;
|
||||
import org.springframework.batch.retry.RetryPolicy;
|
||||
import org.springframework.batch.retry.policy.RecoveryCallbackRetryPolicy;
|
||||
|
||||
/**
|
||||
* A {@link RetryCallback} that knows about and caches an item, and attempts to
|
||||
* process it using a delegate {@link RetryCallback}. Used by the
|
||||
* {@link RecoveryCallbackRetryPolicy} to enable stateful retry of the
|
||||
* processing.
|
||||
*
|
||||
* @author Dave Syer
|
||||
*
|
||||
* @see RecoveryCallbackRetryPolicy
|
||||
* @see RetryPolicy#handleRetryExhausted(RetryContext)
|
||||
*
|
||||
*/
|
||||
public class RecoveryRetryCallback implements RetryCallback {
|
||||
|
||||
private final Object item;
|
||||
|
||||
private final RetryCallback callback;
|
||||
|
||||
private RecoveryCallback recoverer;
|
||||
|
||||
private final Object key;
|
||||
|
||||
private boolean forceRefresh = false;
|
||||
|
||||
/**
|
||||
* Constructor with mandatory properties. The key will be set to the item.
|
||||
*
|
||||
* @param item the item to process
|
||||
* @param callback the delegate to use to process it
|
||||
*/
|
||||
public RecoveryRetryCallback(Object item, RetryCallback callback) {
|
||||
super();
|
||||
this.item = item;
|
||||
this.callback = callback;
|
||||
this.key = item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with mandatory properties.
|
||||
*
|
||||
* @param item the item to process
|
||||
* @param callback the delegate to use to process it
|
||||
*/
|
||||
public RecoveryRetryCallback(Object item, RetryCallback callback, Object key) {
|
||||
super();
|
||||
this.item = item;
|
||||
this.callback = callback;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public getter for the key. This will be used to identify the item being
|
||||
* processed, to see if it has previously failed.
|
||||
* @return the key
|
||||
*/
|
||||
public Object getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for injecting optional recovery handler.
|
||||
*
|
||||
* @param recoverer
|
||||
*/
|
||||
public void setRecoveryCallback(RecoveryCallback recoverer) {
|
||||
this.recoverer = recoverer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public setter for a flag signalling to clients of this callback that the
|
||||
* processing is not a retry. It is always safe to leave this set to the
|
||||
* default value (false), but in some cases it is possible to determine by
|
||||
* examining the input data whether a failure has never been encountered
|
||||
* (e.g. a message header saying that the message has never been consumed).
|
||||
* Clients who have this information can avoid a cache query in such cases
|
||||
* by setting the flag to true.
|
||||
*
|
||||
* @param forceRefresh the flag value to set
|
||||
*/
|
||||
public void setForceRefresh(boolean forceRefresh) {
|
||||
this.forceRefresh = forceRefresh;
|
||||
}
|
||||
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
return callback.doWithRetry(context);
|
||||
// N.B. code used to check here for isExhaustedOnly and throw exception.
|
||||
// This is unnecessary because the callback could just throw the
|
||||
// exception itself if it wants to go to the recovery path.
|
||||
}
|
||||
|
||||
public Object getItem() {
|
||||
return item;
|
||||
}
|
||||
|
||||
public boolean isForceRefresh() {
|
||||
return forceRefresh;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor for the {@link RecoveryCallback}.
|
||||
*
|
||||
* @return the {@link RecoveryCallback}.
|
||||
*/
|
||||
public RecoveryCallback getRecoveryCallback() {
|
||||
return recoverer;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -29,9 +29,8 @@ 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.callback.RecoveryRetryCallback;
|
||||
import org.springframework.batch.retry.RetryState;
|
||||
import org.springframework.batch.retry.policy.NeverRetryPolicy;
|
||||
import org.springframework.batch.retry.policy.RecoveryCallbackRetryPolicy;
|
||||
import org.springframework.batch.retry.support.RetryTemplate;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
@@ -71,7 +70,7 @@ public class StatefulRetryOperationsInterceptor implements MethodInterceptor {
|
||||
*/
|
||||
public StatefulRetryOperationsInterceptor() {
|
||||
super();
|
||||
retryTemplate.setRetryPolicy(new RecoveryCallbackRetryPolicy(new NeverRetryPolicy()));
|
||||
retryTemplate.setRetryPolicy(new NeverRetryPolicy());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,7 +99,7 @@ public class StatefulRetryOperationsInterceptor implements MethodInterceptor {
|
||||
* @param retryPolicy the retryPolicy to set
|
||||
*/
|
||||
public void setRetryPolicy(RetryPolicy retryPolicy) {
|
||||
retryTemplate.setRetryPolicy(new RecoveryCallbackRetryPolicy(retryPolicy));
|
||||
retryTemplate.setRetryPolicy(retryPolicy);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,14 +141,9 @@ public class StatefulRetryOperationsInterceptor implements MethodInterceptor {
|
||||
}
|
||||
final Object item = arg;
|
||||
|
||||
RecoveryRetryCallback callback = new RecoveryRetryCallback(item, new MethodInvocationRetryCallback(invocation),
|
||||
keyGenerator != null ? keyGenerator.getKey(item) : item);
|
||||
callback.setRecoveryCallback(new ItemRecovererCallback(item, recoverer));
|
||||
if (newItemIdentifier != null) {
|
||||
callback.setForceRefresh(newItemIdentifier.isNew(item));
|
||||
}
|
||||
RetryState retryState = new RetryState(keyGenerator != null ? keyGenerator.getKey(item) : item, newItemIdentifier != null ? newItemIdentifier.isNew(item) : false );
|
||||
|
||||
Object result = retryTemplate.execute(callback);
|
||||
Object result = retryTemplate.execute(new MethodInvocationRetryCallback(invocation), new ItemRecovererCallback(item, recoverer), retryState);
|
||||
|
||||
logger.debug("Exiting proxied method in stateful retry with result: (" + result + ")");
|
||||
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
/*
|
||||
* 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.policy;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.batch.retry.ExhaustedRetryException;
|
||||
import org.springframework.batch.retry.RetryContext;
|
||||
import org.springframework.batch.retry.RetryPolicy;
|
||||
|
||||
/**
|
||||
* Base class for stateful retry policies: those that operate in the context of
|
||||
* a callback that is called once per retry execution (usually to enforce that
|
||||
* it is only called once per transaction). Stateful policies need to remember
|
||||
* the context for the operation that failed (e.g. the data item that was being
|
||||
* processed), and decide based on its history what to do in the current
|
||||
* context. For example: the retry operation includes receiving a message, and
|
||||
* we need it to roll back and be re-delivered so that we can have another crack
|
||||
* at it.
|
||||
*
|
||||
* @see RetryPolicy#handleRetryExhausted(RetryContext)
|
||||
* @see AbstractStatelessRetryPolicy
|
||||
*
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
public abstract class AbstractStatefulRetryPolicy implements RetryPolicy {
|
||||
|
||||
private volatile Set<Class<?>> recoverableExceptionClasses = new HashSet<Class<?>>();
|
||||
|
||||
protected RetryContextCache retryContextCache = new MapRetryContextCache();
|
||||
|
||||
/**
|
||||
* Optional setter for the retry context cache. The default value is a
|
||||
* {@link MapRetryContextCache}.
|
||||
*
|
||||
* @param retryContextCache
|
||||
*/
|
||||
public void setRetryContextCache(RetryContextCache retryContextCache) {
|
||||
this.retryContextCache = retryContextCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return null. Subclasses should provide a recovery path if possible.
|
||||
* Subclasses are also encouraged not to declare throws Exception if they
|
||||
* can (e.g. in the plausible and common case that the recovery is a last
|
||||
* ditch effort to prevent a message going back to the middleware, for
|
||||
* instance). Any subclass that actually does throw an Exception of any type
|
||||
* should be aware that it will simply be propagated and the caller will
|
||||
* have top deal with it.
|
||||
*
|
||||
* @throws Exception if the recovery path demands it
|
||||
*
|
||||
* @see org.springframework.batch.retry.RetryPolicy#handleRetryExhausted(org.springframework.batch.retry.RetryContext)
|
||||
*/
|
||||
public Object handleRetryExhausted(RetryContext context) throws ExhaustedRetryException, Exception {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* For a stateful policy the default is to always rethrow. This is the
|
||||
* cautious approach: we assume that the failed processing may have written
|
||||
* data to a transactional resource, so we rethrow and force a rollback. Any
|
||||
* recovery path that may be available has to be taken on the next attempt,
|
||||
* before any processing has taken place.
|
||||
*
|
||||
* @return true unless the last exception registered was recoverable.
|
||||
*/
|
||||
public boolean shouldRethrow(RetryContext context) {
|
||||
return !recoverForException(context.getLastThrowable());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the recoverable exceptions. Any exception on the list, or subclasses
|
||||
* thereof, will be recoverable. If it is encountered in a retry block it
|
||||
* will not be rethrown. Others will be rethrown. The recovery action (if
|
||||
* any) is left to subclasses - normally they would override
|
||||
* {@link #handleRetryExhausted(RetryContext)}.
|
||||
*
|
||||
* @param retryableExceptionClasses defaults to {@link Exception}.
|
||||
*/
|
||||
public final void setRecoverableExceptionClasses(Class<?>[] retryableExceptionClasses) {
|
||||
Set<Class<?>> temp = new HashSet<Class<?>>();
|
||||
for (int i = 0; i < retryableExceptionClasses.length; i++) {
|
||||
addRecoverableExceptionClass(retryableExceptionClasses[i], temp);
|
||||
}
|
||||
this.recoverableExceptionClasses = temp;
|
||||
}
|
||||
|
||||
private void addRecoverableExceptionClass(Class<?> retryableExceptionClass, Set<Class<?>> set) {
|
||||
if (!Throwable.class.isAssignableFrom(retryableExceptionClass)) {
|
||||
throw new IllegalArgumentException("Class '" + retryableExceptionClass.getName()
|
||||
+ "' is not a subtype of Throwable.");
|
||||
}
|
||||
set.add(retryableExceptionClass);
|
||||
}
|
||||
|
||||
protected boolean recoverForException(Throwable ex) {
|
||||
|
||||
// Default is false (but this shouldn't really happen in practice -
|
||||
// maybe in tests):
|
||||
if (ex == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Class<? extends Throwable> exceptionClass = ex.getClass();
|
||||
if (recoverableExceptionClasses.contains(exceptionClass)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// check for subclasses
|
||||
for (Class<?> cls : recoverableExceptionClasses) {
|
||||
if (cls.isAssignableFrom(exceptionClass)) {
|
||||
addRecoverableExceptionClass(exceptionClass, this.recoverableExceptionClasses);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -26,10 +26,11 @@ import org.springframework.batch.retry.RetryPolicy;
|
||||
* state outside the context.
|
||||
*
|
||||
* @see RetryPolicy#handleRetryExhausted(RetryContext)
|
||||
* @see AbstractStatefulRetryPolicy
|
||||
*
|
||||
* @author Dave Syer
|
||||
*
|
||||
* @deprecated TODO: remove this base class
|
||||
*
|
||||
*/
|
||||
public abstract class AbstractStatelessRetryPolicy implements RetryPolicy {
|
||||
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
/*
|
||||
* 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.policy;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.batch.retry.ExhaustedRetryException;
|
||||
import org.springframework.batch.retry.RecoveryCallback;
|
||||
import org.springframework.batch.retry.RetryCallback;
|
||||
import org.springframework.batch.retry.RetryContext;
|
||||
import org.springframework.batch.retry.RetryException;
|
||||
import org.springframework.batch.retry.RetryPolicy;
|
||||
import org.springframework.batch.retry.TerminatedRetryException;
|
||||
import org.springframework.batch.retry.callback.RecoveryRetryCallback;
|
||||
import org.springframework.batch.retry.context.RetryContextSupport;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link RetryPolicy} that detects an {@link RecoveryRetryCallback} when it
|
||||
* opens a new context, and uses it to make sure the item is in place for later
|
||||
* decisions about how to retry or backoff. The callback should be an instance
|
||||
* of {@link RecoveryRetryCallback} otherwise an exception will be thrown when
|
||||
* the context is created.
|
||||
*
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
public class RecoveryCallbackRetryPolicy extends AbstractStatefulRetryPolicy {
|
||||
|
||||
protected Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
public static final String EXHAUSTED = RecoveryCallbackRetryPolicy.class.getName() + ".EXHAUSTED";
|
||||
|
||||
private RetryPolicy delegate;
|
||||
|
||||
/**
|
||||
* Convenience constructor to set delegate on init.
|
||||
*
|
||||
* @param delegate
|
||||
*/
|
||||
public RecoveryCallbackRetryPolicy(RetryPolicy delegate) {
|
||||
super();
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default constructor. Creates a new {@link SimpleRetryPolicy} for the
|
||||
* delegate.
|
||||
*/
|
||||
public RecoveryCallbackRetryPolicy() {
|
||||
this(new SimpleRetryPolicy());
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for delegate.
|
||||
*
|
||||
* @param delegate
|
||||
*/
|
||||
public void setDelegate(RetryPolicy delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the history of this item, and if it has reached the retry limit,
|
||||
* then return false.
|
||||
*
|
||||
* @see org.springframework.batch.retry.RetryPolicy#canRetry(org.springframework.batch.retry.RetryContext)
|
||||
*/
|
||||
public boolean canRetry(RetryContext context) {
|
||||
return ((RetryPolicy) context).canRetry(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to the delegate context.
|
||||
*
|
||||
* @see org.springframework.batch.retry.RetryPolicy#close(org.springframework.batch.retry.RetryContext, boolean)
|
||||
*/
|
||||
public void close(RetryContext context, boolean succeeded) {
|
||||
((RetryPolicy) context).close(context, succeeded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new context for the execution of the callback, which must be an
|
||||
* instance of {@link RecoveryRetryCallback}.
|
||||
*
|
||||
* @see org.springframework.batch.retry.RetryPolicy#open(org.springframework.batch.retry.RetryCallback,
|
||||
* RetryContext)
|
||||
*
|
||||
* @throws IllegalStateException if the callback is not of the required
|
||||
* type.
|
||||
*/
|
||||
public RetryContext open(RetryCallback callback, RetryContext parent) {
|
||||
Assert.state(callback instanceof RecoveryRetryCallback, "Callback must be RecoveryRetryCallback");
|
||||
RecoveryCallbackRetryContext context = new RecoveryCallbackRetryContext((RecoveryRetryCallback) callback, parent);
|
||||
context.open(callback, null);
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* If {@link #canRetry(RetryContext)} is false then take remedial action (if
|
||||
* implemented by subclasses), and remove the current item from the history.
|
||||
*
|
||||
* @see org.springframework.batch.retry.RetryPolicy#registerThrowable(org.springframework.batch.retry.RetryContext,
|
||||
* Exception)
|
||||
*/
|
||||
public void registerThrowable(RetryContext context, Exception throwable) throws TerminatedRetryException {
|
||||
((RetryPolicy) context).registerThrowable(context, throwable);
|
||||
// The throwable is stored in the delegate context.
|
||||
}
|
||||
|
||||
/**
|
||||
* Call recovery path (if any) and clean up context history.
|
||||
*
|
||||
* @see org.springframework.batch.retry.policy.AbstractStatefulRetryPolicy#handleRetryExhausted(org.springframework.batch.retry.RetryContext)
|
||||
*/
|
||||
public Object handleRetryExhausted(RetryContext context) throws Exception, ExhaustedRetryException {
|
||||
return ((RetryPolicy) context).handleRetryExhausted(context);
|
||||
}
|
||||
|
||||
private class RecoveryCallbackRetryContext extends RetryContextSupport implements RetryPolicy {
|
||||
|
||||
final private Object key;
|
||||
|
||||
final private int initialHashCode;
|
||||
|
||||
// The delegate context...
|
||||
private RetryContext delegateContext;
|
||||
|
||||
final private RecoveryCallback recoverer;
|
||||
|
||||
final private boolean forceRefresh;
|
||||
|
||||
public RecoveryCallbackRetryContext(RecoveryRetryCallback callback, RetryContext parent) {
|
||||
super(parent);
|
||||
this.recoverer = callback.getRecoveryCallback();
|
||||
this.key = callback.getKey();
|
||||
this.forceRefresh = callback.isForceRefresh();
|
||||
this.initialHashCode = key.hashCode();
|
||||
}
|
||||
|
||||
public boolean canRetry(RetryContext context) {
|
||||
return delegate.canRetry(this.delegateContext);
|
||||
}
|
||||
|
||||
public void close(RetryContext context, boolean succeeded) {
|
||||
if (succeeded) {
|
||||
retryContextCache.remove(key);
|
||||
delegate.close(this.delegateContext, succeeded);
|
||||
}
|
||||
}
|
||||
|
||||
public RetryContext open(RetryCallback callback, RetryContext parent) {
|
||||
if (forceRefresh) {
|
||||
// Avoid a cache hit if the caller tells us this is a fresh item
|
||||
this.delegateContext = delegate.open(callback, null);
|
||||
}
|
||||
else if (retryContextCache.containsKey(key)) {
|
||||
this.delegateContext = retryContextCache.get(key);
|
||||
if (this.delegateContext == null) {
|
||||
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");
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Only create a new context if we don't know the history of
|
||||
// this item:
|
||||
this.delegateContext = delegate.open(callback, null);
|
||||
}
|
||||
// The return value shouldn't be used...
|
||||
return null;
|
||||
}
|
||||
|
||||
public void registerThrowable(RetryContext context, Exception throwable) throws TerminatedRetryException {
|
||||
// TODO: this comparison assumes that hashCode is the limiting
|
||||
// factor. Actually the cache should be able to decide for us.
|
||||
if (this.initialHashCode != key.hashCode()) {
|
||||
throw new RetryException("Inconsistent state for failed item key: hashCode has changed. "
|
||||
+ "Consider whether equals() or hashCode() for the item might be inconsistent, "
|
||||
+ "or if you need to supply a better ItemKeyGenerator");
|
||||
}
|
||||
retryContextCache.put(key, this.delegateContext);
|
||||
delegate.registerThrowable(this.delegateContext, throwable);
|
||||
}
|
||||
|
||||
public boolean shouldRethrow(RetryContext context) {
|
||||
// Not called...
|
||||
throw new UnsupportedOperationException("Not supported - this code should be unreachable.");
|
||||
}
|
||||
|
||||
public Object handleRetryExhausted(RetryContext context) throws Exception, ExhaustedRetryException {
|
||||
// If there is no going back, then we can remove the history
|
||||
retryContextCache.remove(key);
|
||||
if (recoverer != null) {
|
||||
return recoverer.recover(context);
|
||||
}
|
||||
logger.info("No recovery callback provided. Returning null from recovery step.");
|
||||
// Don't want to call the delegate here - it would throw an exception
|
||||
return null;
|
||||
}
|
||||
|
||||
public Exception getLastThrowable() {
|
||||
return delegateContext.getLastThrowable();
|
||||
}
|
||||
|
||||
public int getRetryCount() {
|
||||
return delegateContext.getRetryCount();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -98,9 +98,6 @@ public class SimpleRetryPolicy extends AbstractStatelessRetryPolicy {
|
||||
public boolean canRetry(RetryContext context) {
|
||||
SimpleRetryContext simpleContext = ((SimpleRetryContext) context);
|
||||
Throwable t = simpleContext.getLastThrowable();
|
||||
// N.B. since the contract is defined to include the initial attempt
|
||||
// in the count, we have to subtract one from the max attempts in this
|
||||
// test
|
||||
return (t == null || retryForException(t)) && simpleContext.getRetryCount() < maxAttempts;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,14 +26,18 @@ import org.springframework.batch.retry.ExhaustedRetryException;
|
||||
import org.springframework.batch.retry.RecoveryCallback;
|
||||
import org.springframework.batch.retry.RetryCallback;
|
||||
import org.springframework.batch.retry.RetryContext;
|
||||
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;
|
||||
import org.springframework.batch.retry.backoff.BackOffPolicy;
|
||||
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;
|
||||
|
||||
/**
|
||||
@@ -72,6 +76,16 @@ public class RetryTemplate implements RetryOperations {
|
||||
|
||||
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
|
||||
@@ -121,9 +135,8 @@ public class RetryTemplate implements RetryOperations {
|
||||
* @throws TerminatedRetryException if the retry has been manually
|
||||
* terminated through the {@link RetryContext}.
|
||||
*/
|
||||
public final Object execute(RetryCallback callback) throws Exception {
|
||||
RetryPolicy retryPolicy = this.retryPolicy;
|
||||
return doExecute(callback, null, retryPolicy);
|
||||
public final Object execute(RetryCallback retryCallback) throws Exception {
|
||||
return doExecute(retryCallback, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,24 +151,52 @@ public class RetryTemplate implements RetryOperations {
|
||||
* terminated through the {@link RetryContext}.
|
||||
*/
|
||||
public final Object execute(RetryCallback retryCallback, RecoveryCallback recoveryCallback) throws Exception {
|
||||
RetryPolicy retryPolicy = this.retryPolicy;
|
||||
return doExecute(retryCallback, recoveryCallback, retryPolicy);
|
||||
return doExecute(retryCallback, recoveryCallback, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param retryCallback
|
||||
* @param recoveryCallback
|
||||
* @param retryPolicy
|
||||
* @return the result of the callback
|
||||
* @throws Exception
|
||||
* Execute the callback once if the policy dictates that we can, re-throwing
|
||||
* any exception encountered.
|
||||
*
|
||||
* @see org.springframework.batch.retry.RetryOperations#execute(RetryCallback,
|
||||
* RetryState)
|
||||
*
|
||||
* @throws ExhaustedRetryException if the retry has been exhausted.
|
||||
*/
|
||||
protected Object doExecute(RetryCallback retryCallback, RecoveryCallback recoveryCallback, RetryPolicy retryPolicy)
|
||||
throws Exception {
|
||||
public final Object 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.
|
||||
*
|
||||
* @see org.springframework.batch.retry.RetryOperations#execute(RetryCallback,
|
||||
* RetryState)
|
||||
*/
|
||||
public final Object 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 org.springframework.batch.retry.RetryOperations#execute(RetryCallback,
|
||||
* RecoveryCallback, RetryState)
|
||||
* @throws ExhaustedRetryException if the retry has been exhausted.
|
||||
*/
|
||||
protected Object 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...
|
||||
// TODO: catch and rethrow abnormal retry exception?
|
||||
RetryContext context = retryPolicy.open(retryCallback, RetrySynchronizationManager.getContext());
|
||||
RetryContext context = open(retryCallback, retryPolicy, state);
|
||||
|
||||
// Make sure the context is available globally for clients who need
|
||||
// it...
|
||||
@@ -196,9 +237,9 @@ public class RetryTemplate implements RetryOperations {
|
||||
|
||||
doOnErrorInterceptors(retryCallback, context, e);
|
||||
|
||||
retryPolicy.registerThrowable(context, e);
|
||||
registerThrowable(retryPolicy, state, context, e);
|
||||
|
||||
if (shouldRethrow(context)) {
|
||||
if (shouldRethrow(retryPolicy, context, state)) {
|
||||
logger.debug("Rethrow in retry for policy: count=" + context.getRetryCount());
|
||||
throw e;
|
||||
}
|
||||
@@ -230,44 +271,139 @@ public class RetryTemplate implements RetryOperations {
|
||||
.getLastThrowable());
|
||||
}
|
||||
|
||||
return handleRetryExhausted(recoveryCallback, context);
|
||||
return handleRetryExhausted(recoveryCallback, context, state);
|
||||
|
||||
}
|
||||
finally {
|
||||
retryPolicy.close(context, lastException == null);
|
||||
close(retryPolicy, context, state, lastException == null);
|
||||
doCloseInterceptors(retryCallback, context, lastException);
|
||||
RetrySynchronizationManager.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @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, succeeded);
|
||||
}
|
||||
}
|
||||
else {
|
||||
retryPolicy.close(context, succeeded);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param retryPolicy
|
||||
* @param state
|
||||
* @param context
|
||||
* @param e
|
||||
*/
|
||||
protected void registerThrowable(RetryPolicy retryPolicy, RetryState state, RetryContext context, Exception e) {
|
||||
if (state != null) {
|
||||
Object key = state.getKey();
|
||||
// TODO: this comparison assumes that hashCode is the limiting
|
||||
// factor. Actually the cache should be able to decide for us.
|
||||
// if (initialHashCode != key.hashCode()) {
|
||||
// throw new RetryException(
|
||||
// "Inconsistent state for failed item key: hashCode has changed. "
|
||||
// +
|
||||
// "Consider whether equals() or hashCode() for the item might be inconsistent, "
|
||||
// + "or if you need to supply a better ItemKeyGenerator");
|
||||
// }
|
||||
retryContextCache.put(key, context);
|
||||
}
|
||||
retryPolicy.registerThrowable(context, e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param retryCallback
|
||||
* @param retryPolicy
|
||||
* @return a retry context
|
||||
*/
|
||||
protected RetryContext open(RetryCallback retryCallback, RetryPolicy retryPolicy, RetryState state) {
|
||||
|
||||
// TODO: we don't need the callback here
|
||||
|
||||
if (state == null) {
|
||||
return doOpenInternal(retryCallback, retryPolicy);
|
||||
}
|
||||
|
||||
Object key = state.getKey();
|
||||
if (state.isForceRefresh()) {
|
||||
return doOpenInternal(retryCallback, retryPolicy);
|
||||
}
|
||||
else if (retryContextCache.containsKey(key)) {
|
||||
|
||||
RetryContext context = retryContextCache.get(key);
|
||||
if (context == null) {
|
||||
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");
|
||||
}
|
||||
return context;
|
||||
|
||||
}
|
||||
else {
|
||||
|
||||
// The cache is only ued if there is a failure.
|
||||
return doOpenInternal(retryCallback, retryPolicy);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param retryCallback
|
||||
* @param retryPolicy
|
||||
* @return
|
||||
*/
|
||||
private RetryContext doOpenInternal(RetryCallback retryCallback, RetryPolicy retryPolicy) {
|
||||
return retryPolicy.open(retryCallback, RetrySynchronizationManager.getContext());
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 then
|
||||
* definitely the last exception from the context
|
||||
*/
|
||||
private Object handleRetryExhausted(RecoveryCallback recoveryCallback, RetryContext context) throws Exception {
|
||||
return retryPolicy.handleRetryExhausted(context);
|
||||
// if (recoveryCallback != null) {
|
||||
// return recoveryCallback.recover(context);
|
||||
// }
|
||||
// logger.debug("Retry exhausted after last attempt with no recovery path.");
|
||||
// throw context.getLastThrowable();
|
||||
protected Object handleRetryExhausted(RecoveryCallback recoveryCallback, RetryContext context, RetryState state)
|
||||
throws Exception {
|
||||
if (state != null) {
|
||||
retryContextCache.remove(state.getKey());
|
||||
}
|
||||
// TODO: test this when state==null
|
||||
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 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.
|
||||
* to rethrow, and if there is state we rethrow if the policy can still
|
||||
* retry.
|
||||
*
|
||||
* @param context the current {@link RetryContext}
|
||||
* @param retryPolicy
|
||||
* @param context the current context
|
||||
*
|
||||
* @return false but subclasses might choose otherwise
|
||||
*/
|
||||
protected boolean shouldRethrow(RetryContext context) {
|
||||
// TODO: return false
|
||||
return retryPolicy.shouldRethrow(context);
|
||||
protected boolean shouldRethrow(RetryPolicy retryPolicy, RetryContext context, RetryState state) {
|
||||
return state != null;
|
||||
}
|
||||
|
||||
private boolean doOpenInterceptors(RetryCallback callback, RetryContext context) {
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* 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.RetryContext;
|
||||
import org.springframework.batch.retry.RetryPolicy;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Dave Syer
|
||||
*/
|
||||
public class StatefulRetryTemplate extends RetryTemplate {
|
||||
|
||||
/**
|
||||
* @param retryCallback
|
||||
* @param recoveryCallback
|
||||
* @param retryPolicy
|
||||
* @return the result of the callback
|
||||
* @throws Exception
|
||||
*/
|
||||
protected Object doExecute(RetryCallback retryCallback, RecoveryCallback recoveryCallback, RetryPolicy retryPolicy)
|
||||
throws Exception {
|
||||
|
||||
return super.doExecute(retryCallback, recoveryCallback, retryPolicy);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension point for subclasses to decide on behaviour after catching an
|
||||
* exception in a {@link RetryCallback}. Normal stateless behaviour is not
|
||||
* to rethrow.
|
||||
*
|
||||
* @param context the current {@link RetryContext}
|
||||
*
|
||||
* @return false but subclasses might choose otherwise
|
||||
*/
|
||||
protected boolean shouldRethrow(RetryContext context) {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
/*
|
||||
* 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.callback;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.springframework.batch.retry.RetryCallback;
|
||||
import org.springframework.batch.retry.RetryContext;
|
||||
import org.springframework.batch.retry.RetryException;
|
||||
import org.springframework.batch.retry.StubItemKeyGeneratorRecoverer;
|
||||
import org.springframework.batch.retry.TerminatedRetryException;
|
||||
import org.springframework.batch.retry.context.RetryContextSupport;
|
||||
import org.springframework.batch.retry.policy.NeverRetryPolicy;
|
||||
import org.springframework.batch.retry.support.RetryTemplate;
|
||||
|
||||
public class RecoveryRetryCallbackTests extends TestCase {
|
||||
|
||||
List<Object> calls = new ArrayList<Object>();
|
||||
|
||||
int count = 0;
|
||||
|
||||
RetryTemplate template;
|
||||
|
||||
StubItemKeyGeneratorRecoverer recoverer;
|
||||
|
||||
RecoveryRetryCallback callback;
|
||||
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
template = new RetryTemplate();
|
||||
recoverer = new StubItemKeyGeneratorRecoverer() {
|
||||
public Object recover(Object data, Throwable cause) {
|
||||
count++;
|
||||
calls.add(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
public Object getKey(Object item) {
|
||||
return "key" + (count++);
|
||||
}
|
||||
};
|
||||
callback = new RecoveryRetryCallback("foo", new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
count++;
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void testDoWithRetrySuccessfulFirstTime() throws Exception {
|
||||
template.execute(callback);
|
||||
assertEquals(1, count);
|
||||
}
|
||||
|
||||
public void testContextInitializedWithItemAndCanRetry() throws Exception {
|
||||
// We can use the policy to intercept the context and do something with
|
||||
// the item...
|
||||
callback = new RecoveryRetryCallback("bar", new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
count++;
|
||||
throw new IllegalStateException("Detected bar");
|
||||
}
|
||||
});
|
||||
assertEquals(0, calls.size());
|
||||
template.setRetryPolicy(new NeverRetryPolicy() {
|
||||
public boolean canRetry(RetryContext context) {
|
||||
// ...register the failed item
|
||||
calls.add("item(" + count + ")=" + callback.getKey());
|
||||
// Do not call the base class method - the attempt counts as
|
||||
// successful now
|
||||
if (count < 2) // only retry once
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
try {
|
||||
template.execute(callback);
|
||||
fail("Expected IllegalStateException");
|
||||
}
|
||||
catch (IllegalStateException e) {
|
||||
// expected
|
||||
}
|
||||
System.err.println(calls);
|
||||
assertEquals(2, count);
|
||||
// Two from initial attempt (one in shouldRethrow and one at start of
|
||||
// do loop), two from the final attempt (same)...
|
||||
assertEquals(4, calls.size());
|
||||
assertEquals("item(1)=bar", calls.get(1));
|
||||
}
|
||||
|
||||
public void testContextInitializedWithItemAndRegisterThrowable() throws Exception {
|
||||
// We can use the policy to intercept the context and do something with
|
||||
// the item...
|
||||
callback = new RecoveryRetryCallback("bar", new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
count++;
|
||||
throw new IllegalStateException("Detected bar");
|
||||
}
|
||||
});
|
||||
assertEquals(0, calls.size());
|
||||
template.setRetryPolicy(new NeverRetryPolicy() {
|
||||
public void registerThrowable(RetryContext context, Exception throwable) throws TerminatedRetryException {
|
||||
// ...register the failed item
|
||||
calls.add("item=" + callback.getKey());
|
||||
// Call the base class method so that the next attempt is a
|
||||
// failure.
|
||||
super.registerThrowable(context, throwable);
|
||||
}
|
||||
});
|
||||
try {
|
||||
template.execute(callback);
|
||||
fail("Expected IllegalStateException");
|
||||
}
|
||||
catch (IllegalStateException e) {
|
||||
// expected
|
||||
}
|
||||
// One call from the callback itself and one from the retry policy
|
||||
assertEquals(1, count);
|
||||
assertEquals(1, calls.size());
|
||||
assertEquals("item=bar", calls.get(0));
|
||||
}
|
||||
|
||||
public void testContextMarkedExhausted() throws Exception {
|
||||
RetryContext context = new RetryContextSupport(null);
|
||||
context.setExhaustedOnly();
|
||||
try {
|
||||
callback.doWithRetry(context);
|
||||
}
|
||||
catch (Throwable t) {
|
||||
assertTrue(t instanceof RetryException);
|
||||
}
|
||||
}
|
||||
|
||||
public void testGetKey() throws Exception {
|
||||
callback = new RecoveryRetryCallback("foo", null, "key0");
|
||||
assertEquals("key0", callback.getKey());
|
||||
}
|
||||
|
||||
public void testRecoverWithoutSession() throws Exception {
|
||||
recoverer.recover("foo", null);
|
||||
assertEquals(1, count);
|
||||
assertEquals(1, calls.size());
|
||||
}
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
/*
|
||||
* 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.policy;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.springframework.batch.retry.RetryCallback;
|
||||
import org.springframework.batch.retry.RetryContext;
|
||||
import org.springframework.batch.retry.TerminatedRetryException;
|
||||
import org.springframework.batch.retry.context.RetryContextSupport;
|
||||
import org.springframework.batch.retry.support.RetryTemplate;
|
||||
|
||||
public class ExternalRetryPolicyTests extends TestCase {
|
||||
|
||||
public void testExternalRetryStopsLoop() throws Exception {
|
||||
MockRetryCallback callback = new MockRetryCallback();
|
||||
callback.setExceptionToThrow(new IllegalArgumentException());
|
||||
|
||||
RetryTemplate retryTemplate = new RetryTemplate();
|
||||
retryTemplate.setRetryPolicy(new MockExternalRetryPolicy(3));
|
||||
|
||||
Object result = "start_foo";
|
||||
try {
|
||||
result = retryTemplate.execute(callback);
|
||||
// If template is external and retry is still permitted, then
|
||||
// we expect the exception to be propagated.
|
||||
fail("Expected IllegalArgumentException");
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
assertNull(e.getMessage());
|
||||
}
|
||||
assertEquals(1, callback.attempts);
|
||||
assertEquals("start_foo", result);
|
||||
}
|
||||
|
||||
public void testExternalRetryWithFailAndNoRetry() throws Exception {
|
||||
MockRetryCallback callback = new MockRetryCallback();
|
||||
callback.setExceptionToThrow(new IllegalArgumentException());
|
||||
|
||||
RetryTemplate retryTemplate = new RetryTemplate();
|
||||
|
||||
// Allow one unsuccessful attempt (plus one for recovery):
|
||||
retryTemplate.setRetryPolicy(new MockExternalRetryPolicy(1));
|
||||
|
||||
Object result = "start_foo";
|
||||
try {
|
||||
result = retryTemplate.execute(callback);
|
||||
// The first failed attempt we expect to retry...
|
||||
fail("Expected IllegalArgumentException");
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
assertNull(e.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
result = retryTemplate.execute(callback);
|
||||
// We always get a second attempt...
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
// This is now the "exhausted" message:
|
||||
assertNotNull(e.getMessage());
|
||||
// But if template is external we should
|
||||
// swallow the exception when retry is impossible.
|
||||
fail("Did not expect IllegalArgumentException");
|
||||
}
|
||||
// Callback is called once: the recovery path should be called in
|
||||
// handleRetryExhausted (so not in this test)...
|
||||
assertEquals(1, callback.attempts);
|
||||
assertEquals(null, result);
|
||||
}
|
||||
|
||||
public void testNonThrowableIsNotRecoverable() throws Exception {
|
||||
|
||||
try {
|
||||
MockExternalRetryPolicy policy = new MockExternalRetryPolicy(1);
|
||||
policy.setRecoverableExceptionClasses(new Class[] { String.class });
|
||||
fail("Expected IllegalArgumentException");
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
// Expected
|
||||
assertTrue(Pattern.matches(".*not.*Throwable.*", e.getMessage()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void testSuclassIsRecoverable() throws Exception {
|
||||
|
||||
MockExternalRetryPolicy policy = new MockExternalRetryPolicy(1);
|
||||
policy.setRecoverableExceptionClasses(new Class[] { IllegalArgumentException.class });
|
||||
|
||||
RetryContextSupport context = new RetryContextSupport(null);
|
||||
|
||||
assertTrue(policy.shouldRethrow(context));
|
||||
|
||||
context.registerThrowable(new IllegalStateException());
|
||||
assertTrue(policy.shouldRethrow(context));
|
||||
|
||||
context.registerThrowable(new IllegalArgumentException());
|
||||
assertFalse(policy.shouldRethrow(context));
|
||||
|
||||
context.registerThrowable(new IllegalArgumentException() {
|
||||
// subclass
|
||||
});
|
||||
assertFalse(policy.shouldRethrow(context));
|
||||
|
||||
}
|
||||
|
||||
public void testRecoverableException() throws Exception {
|
||||
MockRetryCallback callback = new MockRetryCallback();
|
||||
callback.setExceptionToThrow(new IllegalArgumentException());
|
||||
|
||||
RetryTemplate retryTemplate = new RetryTemplate();
|
||||
|
||||
// Allow one unsuccessful attempt (should take recovery path):
|
||||
MockExternalRetryPolicy policy = new MockExternalRetryPolicy(1);
|
||||
policy
|
||||
.setRecoverableExceptionClasses(new Class[] { IllegalArgumentException.class,
|
||||
IllegalStateException.class });
|
||||
retryTemplate.setRetryPolicy(policy);
|
||||
|
||||
Object result = "start_foo";
|
||||
try {
|
||||
result = retryTemplate.execute(callback);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
// This is the "exhausted" message:
|
||||
assertNotNull(e.getMessage());
|
||||
// But if template is external we should
|
||||
// swallow the exception when retry is impossible.
|
||||
fail("Did not expect IllegalArgumentException");
|
||||
}
|
||||
// Callback is called once: the recovery path should be called in
|
||||
// handleRetryExhausted (so not in this test)...
|
||||
assertEquals(1, callback.attempts);
|
||||
assertEquals(null, result);
|
||||
}
|
||||
|
||||
private static class MockRetryCallback implements RetryCallback {
|
||||
|
||||
private int attempts;
|
||||
|
||||
public static String EXHAUSTED = "complete";
|
||||
|
||||
private Exception exceptionToThrow = new Exception();
|
||||
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
this.attempts++;
|
||||
if (((Boolean) context.getAttribute(EXHAUSTED)).booleanValue()) {
|
||||
// This is now a recovery step...
|
||||
return null;
|
||||
}
|
||||
// Otherwise just barf...
|
||||
throw this.exceptionToThrow;
|
||||
}
|
||||
|
||||
public void setExceptionToThrow(Exception exceptionToThrow) {
|
||||
this.exceptionToThrow = exceptionToThrow;
|
||||
}
|
||||
}
|
||||
|
||||
private static class MockExternalRetryPolicy extends AbstractStatefulRetryPolicy {
|
||||
|
||||
private int retryLimit = 0;
|
||||
|
||||
// This one is stateful for testing only - normally this state would
|
||||
// have to be managed by the context.
|
||||
private int attempts = 0;
|
||||
|
||||
public MockExternalRetryPolicy(int retryLimit) {
|
||||
super();
|
||||
this.retryLimit = retryLimit;
|
||||
}
|
||||
|
||||
public boolean canRetry(RetryContext context) {
|
||||
return attempts < retryLimit;
|
||||
}
|
||||
|
||||
public void close(RetryContext context, boolean succeeded) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
public RetryContext open(RetryCallback callback, RetryContext parent) {
|
||||
RetryContextSupport context = new RetryContextSupport(null);
|
||||
context.setAttribute(MockRetryCallback.EXHAUSTED, Boolean.valueOf(!canRetry(context)));
|
||||
return context;
|
||||
}
|
||||
|
||||
public void registerThrowable(RetryContext context, Exception throwable) throws TerminatedRetryException {
|
||||
((RetryContextSupport) context).registerThrowable(throwable);
|
||||
attempts++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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.policy;
|
||||
|
||||
import java.util.HashSet;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.springframework.batch.retry.RecoveryCallback;
|
||||
import org.springframework.batch.retry.RetryCallback;
|
||||
import org.springframework.batch.retry.RetryContext;
|
||||
import org.springframework.batch.retry.RetryState;
|
||||
import org.springframework.batch.retry.support.RetryTemplate;
|
||||
|
||||
public class FatalExceptionRetryPolicyTests extends TestCase {
|
||||
|
||||
public void testFatalExceptionWithoutState() throws Exception {
|
||||
MockRetryCallback callback = new MockRetryCallback();
|
||||
callback.setExceptionToThrow(new IllegalArgumentException());
|
||||
|
||||
RetryTemplate retryTemplate = new RetryTemplate();
|
||||
// Allow multiple attempts in general...
|
||||
SimpleRetryPolicy policy = new SimpleRetryPolicy(3);
|
||||
retryTemplate.setRetryPolicy(policy);
|
||||
|
||||
// ...but make sure certain exceptions are fatal
|
||||
policy.setFatalExceptionClasses(new HashSet<Class<? extends Throwable>>() {
|
||||
{
|
||||
add(IllegalArgumentException.class);
|
||||
add(IllegalStateException.class);
|
||||
}
|
||||
});
|
||||
RecoveryCallback recoveryCallback = new RecoveryCallback() {
|
||||
public Object recover(RetryContext context) throws Exception {
|
||||
return "bar";
|
||||
}
|
||||
};
|
||||
|
||||
Object result = null;
|
||||
try {
|
||||
result = retryTemplate.execute(callback, recoveryCallback);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
// We should swallow the exception when recovery is possible
|
||||
fail("Did not expect IllegalArgumentException");
|
||||
}
|
||||
// Callback is called once: the recovery path should also be called
|
||||
assertEquals(1, callback.attempts);
|
||||
assertEquals("bar", result);
|
||||
}
|
||||
|
||||
public void testFatalExceptionWithState() throws Exception {
|
||||
MockRetryCallback callback = new MockRetryCallback();
|
||||
callback.setExceptionToThrow(new IllegalArgumentException());
|
||||
|
||||
RetryTemplate retryTemplate = new RetryTemplate();
|
||||
SimpleRetryPolicy policy = new SimpleRetryPolicy(3);
|
||||
retryTemplate.setRetryPolicy(policy);
|
||||
|
||||
policy.setFatalExceptionClasses(new HashSet<Class<? extends Throwable>>() {
|
||||
{
|
||||
add(IllegalArgumentException.class);
|
||||
add(IllegalStateException.class);
|
||||
}
|
||||
});
|
||||
RecoveryCallback recoveryCallback = new RecoveryCallback() {
|
||||
public Object recover(RetryContext context) throws Exception {
|
||||
return "bar";
|
||||
}
|
||||
};
|
||||
|
||||
Object result = null;
|
||||
try {
|
||||
retryTemplate.execute(callback, recoveryCallback, new RetryState("foo"));
|
||||
fail("Expected IllegalArgumentException");
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
// If stateful we have to always rethrow. Clients who want special
|
||||
// cases have to implement them in the callback
|
||||
}
|
||||
result = retryTemplate.execute(callback, recoveryCallback, new RetryState("foo"));
|
||||
// Callback is called once: the recovery path should also be called
|
||||
assertEquals(1, callback.attempts);
|
||||
assertEquals("bar", result);
|
||||
}
|
||||
|
||||
private static class MockRetryCallback implements RetryCallback {
|
||||
|
||||
private int attempts;
|
||||
|
||||
private Exception exceptionToThrow = new Exception();
|
||||
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
this.attempts++;
|
||||
// Otherwise just barf...
|
||||
throw this.exceptionToThrow;
|
||||
}
|
||||
|
||||
public void setExceptionToThrow(Exception exceptionToThrow) {
|
||||
this.exceptionToThrow = exceptionToThrow;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,409 +0,0 @@
|
||||
/*
|
||||
* 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.policy;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.springframework.batch.repeat.RepeatContext;
|
||||
import org.springframework.batch.repeat.context.RepeatContextSupport;
|
||||
import org.springframework.batch.repeat.support.RepeatSynchronizationManager;
|
||||
import org.springframework.batch.retry.RecoveryCallback;
|
||||
import org.springframework.batch.retry.RetryCallback;
|
||||
import org.springframework.batch.retry.RetryContext;
|
||||
import org.springframework.batch.retry.RetryException;
|
||||
import org.springframework.batch.retry.callback.RecoveryRetryCallback;
|
||||
import org.springframework.batch.retry.support.RetryTemplate;
|
||||
|
||||
public class RecoveryRetryPolicyTests extends TestCase {
|
||||
|
||||
private RecoveryCallbackRetryPolicy policy = new RecoveryCallbackRetryPolicy();
|
||||
|
||||
private int count = 0;
|
||||
|
||||
private List<String> list = new ArrayList<String>();
|
||||
|
||||
public void testOpenSunnyDay() throws Exception {
|
||||
|
||||
final StringHolder item = new StringHolder("foo");
|
||||
RetryCallback writer = new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
count++;
|
||||
list.add(item.string);
|
||||
return item;
|
||||
}
|
||||
};
|
||||
|
||||
RetryContext context = policy.open(new RecoveryRetryCallback("foo", writer), null);
|
||||
assertNotNull(context);
|
||||
// we haven't called the processor yet...
|
||||
assertEquals(0, count);
|
||||
}
|
||||
|
||||
public void testOpenWithWrongCallbackType() {
|
||||
try {
|
||||
policy.open(new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
return null;
|
||||
}
|
||||
}, null);
|
||||
fail("Expected IllegalStateException");
|
||||
}
|
||||
catch (IllegalStateException e) {
|
||||
assertTrue(e.getMessage().indexOf("must be RecoveryRetryCallback") >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void testCanRetry() {
|
||||
policy.setDelegate(new AlwaysRetryPolicy());
|
||||
|
||||
RetryContext context = policy.open(new RecoveryRetryCallback("foo", new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
count++;
|
||||
return null;
|
||||
}
|
||||
}), null);
|
||||
assertNotNull(context);
|
||||
|
||||
// We can always retry if delegate says so...
|
||||
assertTrue(policy.canRetry(context));
|
||||
}
|
||||
|
||||
public void testRegisterThrowable() {
|
||||
policy.setDelegate(new NeverRetryPolicy());
|
||||
RetryContext context = policy.open(new RecoveryRetryCallback("foo", new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
count++;
|
||||
return null;
|
||||
}
|
||||
}), null);
|
||||
assertNotNull(context);
|
||||
policy.registerThrowable(context, new Exception());
|
||||
assertFalse(policy.canRetry(context));
|
||||
}
|
||||
|
||||
public void testClose() throws Exception {
|
||||
policy.setDelegate(new NeverRetryPolicy());
|
||||
RetryContext context = policy.open(new RecoveryRetryCallback("foo", new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
count++;
|
||||
return null;
|
||||
}
|
||||
}), null);
|
||||
assertNotNull(context);
|
||||
policy.registerThrowable(context, new Exception());
|
||||
assertFalse(policy.canRetry(context));
|
||||
policy.close(context, true);
|
||||
// still can't retry, even if policy is closed
|
||||
// (not that this would happen in practice)...
|
||||
assertFalse(policy.canRetry(context));
|
||||
}
|
||||
|
||||
public void testOpenTwice() throws Exception {
|
||||
RecoveryRetryCallback callback = new RecoveryRetryCallback("foo", new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
count++;
|
||||
return null;
|
||||
}
|
||||
});
|
||||
policy.setDelegate(new SimpleRetryPolicy(2));
|
||||
|
||||
// First call...
|
||||
RetryContext context = policy.open(callback, null);
|
||||
assertNotNull(context);
|
||||
policy.registerThrowable(context, new Exception());
|
||||
assertTrue(policy.canRetry(context));
|
||||
policy.close(context, false);
|
||||
|
||||
// Second call...
|
||||
context = policy.open(callback, null);
|
||||
assertNotNull(context);
|
||||
policy.registerThrowable(context, new Exception());
|
||||
assertFalse(policy.canRetry(context));
|
||||
policy.close(context, true);
|
||||
|
||||
}
|
||||
|
||||
public void testRecover() throws Exception {
|
||||
policy = new RecoveryCallbackRetryPolicy();
|
||||
policy.setDelegate(new SimpleRetryPolicy(1));
|
||||
final String input = "foo";
|
||||
RecoveryRetryCallback callback = new RecoveryRetryCallback(input, new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
callback.setRecoveryCallback(new RecoveryCallback() {
|
||||
public Object recover(RetryContext context) {
|
||||
count++;
|
||||
list.add(input);
|
||||
return input;
|
||||
}
|
||||
});
|
||||
RetryContext context = policy.open(callback, null);
|
||||
assertNotNull(context);
|
||||
assertTrue(policy.canRetry(context));
|
||||
policy.registerThrowable(context, new Exception());
|
||||
assertFalse(policy.canRetry(context));
|
||||
assertEquals(0, count);
|
||||
context = policy.open(callback, null);
|
||||
// On the second retry, the recovery path is taken...
|
||||
Object result = policy.handleRetryExhausted(context);
|
||||
assertEquals("foo", result); // the recoverer returns the item
|
||||
assertEquals(1, count);
|
||||
assertFalse(policy.canRetry(context));
|
||||
assertEquals("foo", list.get(0));
|
||||
}
|
||||
|
||||
public void testRecoverWithParent() throws Exception {
|
||||
RepeatContext parent = new RepeatContextSupport(null);
|
||||
RepeatSynchronizationManager.register(new RepeatContextSupport(parent));
|
||||
testRecover();
|
||||
assertFalse(parent.isCompleteOnly());
|
||||
RepeatSynchronizationManager.clear();
|
||||
}
|
||||
|
||||
public void testRecoverWithTemplate() throws Exception {
|
||||
policy = new RecoveryCallbackRetryPolicy();
|
||||
policy.setDelegate(new SimpleRetryPolicy(1));
|
||||
final String input = "foo";
|
||||
RecoveryRetryCallback callback = new RecoveryRetryCallback(input, new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
throw new RuntimeException("Barf!");
|
||||
}
|
||||
});
|
||||
callback.setRecoveryCallback(new RecoveryCallback() {
|
||||
public Object recover(RetryContext context) {
|
||||
count++;
|
||||
list.add(input);
|
||||
return input;
|
||||
}
|
||||
});
|
||||
RetryTemplate template = new RetryTemplate();
|
||||
template.setRetryPolicy(policy);
|
||||
Object result = null;
|
||||
try {
|
||||
result = template.execute(callback);
|
||||
fail("Expected exception on first try");
|
||||
}
|
||||
catch (Exception e) {
|
||||
// expected...
|
||||
}
|
||||
// On the second retry, the recovery path is taken...
|
||||
result = template.execute(callback);
|
||||
assertEquals(input, result); // default result is the item
|
||||
assertEquals(1, count);
|
||||
assertEquals(input, list.get(0));
|
||||
}
|
||||
|
||||
public void testExhaustedClearsHistoryAfterLastAttempt() throws Exception {
|
||||
RecoveryRetryCallback callback = new RecoveryRetryCallback("foo", new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
count++;
|
||||
return null;
|
||||
}
|
||||
});
|
||||
policy.setDelegate(new SimpleRetryPolicy(1));
|
||||
|
||||
RetryContext context = policy.open(callback, null);
|
||||
assertNotNull(context);
|
||||
|
||||
assertEquals(0, count);
|
||||
policy.registerThrowable(context, new Exception());
|
||||
|
||||
// False before close...
|
||||
assertFalse(policy.canRetry(context));
|
||||
policy.close(context, true);
|
||||
Object result = policy.handleRetryExhausted(context);
|
||||
assertNull(result); // default result is null
|
||||
|
||||
context = policy.open(callback, null);
|
||||
// True after exhausted - the history is reset...
|
||||
assertTrue(policy.canRetry(context));
|
||||
}
|
||||
|
||||
public void testRetryCount() throws Exception {
|
||||
policy = new RecoveryCallbackRetryPolicy();
|
||||
policy.setDelegate(new SimpleRetryPolicy(1));
|
||||
RetryContext context = policy.open(new RecoveryRetryCallback("foo", new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
count++;
|
||||
return null;
|
||||
}
|
||||
}), null);
|
||||
assertNotNull(context);
|
||||
policy.registerThrowable(context, null);
|
||||
assertEquals(0, context.getRetryCount());
|
||||
policy.registerThrowable(context, new RuntimeException("foo"));
|
||||
assertEquals(1, context.getRetryCount());
|
||||
assertEquals("foo", context.getLastThrowable().getMessage());
|
||||
}
|
||||
|
||||
public void testRetryCountPreservedBetweenRetries() throws Exception {
|
||||
RecoveryRetryCallback callback = new RecoveryRetryCallback("bar", new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
count++;
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
policy = new RecoveryCallbackRetryPolicy();
|
||||
policy.setDelegate(new SimpleRetryPolicy(1));
|
||||
RetryContext context = policy.open(callback, null);
|
||||
assertNotNull(context);
|
||||
policy.registerThrowable(context, new RuntimeException("foo"));
|
||||
assertEquals(1, context.getRetryCount());
|
||||
context = policy.open(callback, null);
|
||||
assertEquals(1, context.getRetryCount());
|
||||
policy.registerThrowable(context, new RuntimeException("foo"));
|
||||
assertEquals(2, context.getRetryCount());
|
||||
}
|
||||
|
||||
public void testKeyGeneratorNotConsistentAfterFailure() throws Throwable {
|
||||
|
||||
policy = new RecoveryCallbackRetryPolicy();
|
||||
policy.setDelegate(new SimpleRetryPolicy(3));
|
||||
final StringHolder item = new StringHolder("bar");
|
||||
|
||||
RetryCallback writer = new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
// This simulates what happens if someone uses a primary key
|
||||
// for hasCode and equals and then relies on default key
|
||||
// generator
|
||||
((StringHolder) item).string = ((StringHolder) item).string + (count++);
|
||||
throw new RuntimeException("Barf!");
|
||||
}
|
||||
};
|
||||
|
||||
RecoveryRetryCallback callback = new RecoveryRetryCallback(item, writer);
|
||||
RetryContext context = policy.open(callback, null);
|
||||
assertNotNull(context);
|
||||
try {
|
||||
callback.doWithRetry(context);
|
||||
fail("Expected RuntimeException");
|
||||
}
|
||||
catch (RuntimeException e) {
|
||||
assertEquals("Barf!", e.getMessage());
|
||||
try {
|
||||
policy.registerThrowable(context, e);
|
||||
fail("Expected RetryException");
|
||||
}
|
||||
catch (RetryException ex) {
|
||||
String message = ex.getMessage();
|
||||
assertTrue("Message doesn't contain 'inconsistent': " + message, message.indexOf("inconsistent") >= 0);
|
||||
}
|
||||
assertEquals(0, context.getRetryCount());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void testCacheCapacity() throws Exception {
|
||||
policy = new RecoveryCallbackRetryPolicy();
|
||||
policy.setDelegate(new SimpleRetryPolicy(1));
|
||||
policy.setRetryContextCache(new MapRetryContextCache(1));
|
||||
final StringHolder item = new StringHolder("foo");
|
||||
|
||||
RetryCallback writer = new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
count++;
|
||||
list.add(item.string);
|
||||
return item;
|
||||
}
|
||||
};
|
||||
RetryContext context;
|
||||
context = policy.open(new RecoveryRetryCallback(item, writer), null);
|
||||
policy.registerThrowable(context, null);
|
||||
assertEquals(0, context.getRetryCount());
|
||||
item.string = "bar";
|
||||
context = policy.open(new RecoveryRetryCallback(item, writer), null);
|
||||
try {
|
||||
policy.registerThrowable(context, new RuntimeException("foo"));
|
||||
fail("Expected RetryException");
|
||||
}
|
||||
catch (RetryException e) {
|
||||
String message = e.getMessage();
|
||||
assertTrue("Message does not contain 'capacity': " + message, message.indexOf("capacity") >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void testCacheCapacityNotReachedIfRecovered() throws Exception {
|
||||
policy = new RecoveryCallbackRetryPolicy();
|
||||
policy.setDelegate(new SimpleRetryPolicy(1));
|
||||
policy.setRetryContextCache(new MapRetryContextCache(2));
|
||||
final StringHolder item = new StringHolder("foo");
|
||||
|
||||
RetryCallback writer = new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
count++;
|
||||
list.add(item.string);
|
||||
return item;
|
||||
}
|
||||
};
|
||||
|
||||
RetryContext context;
|
||||
context = policy.open(new RecoveryRetryCallback(item, writer), null);
|
||||
policy.registerThrowable(context, null);
|
||||
assertEquals(0, context.getRetryCount());
|
||||
policy.registerThrowable(context, new RuntimeException("foo"));
|
||||
context = policy.open(new RecoveryRetryCallback("bar", writer), null);
|
||||
policy.registerThrowable(context, null);
|
||||
policy.handleRetryExhausted(context);
|
||||
context = policy.open(new RecoveryRetryCallback("spam", writer), null);
|
||||
policy.registerThrowable(context, null);
|
||||
assertEquals(0, context.getRetryCount());
|
||||
}
|
||||
|
||||
private static class StringHolder {
|
||||
|
||||
private String string;
|
||||
|
||||
/**
|
||||
* @param string
|
||||
*/
|
||||
public StringHolder(String string) {
|
||||
this.string = string;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
*/
|
||||
public boolean equals(Object obj) {
|
||||
return string.equals(((StringHolder) obj).string);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#hashCode()
|
||||
*/
|
||||
public int hashCode() {
|
||||
return string.hashCode();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
public String toString() {
|
||||
return "String: " + string + " (hash = " + hashCode() + ")";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,9 +23,10 @@ import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.batch.retry.ExhaustedRetryException;
|
||||
import org.springframework.batch.retry.RetryCallback;
|
||||
import org.springframework.batch.retry.RetryContext;
|
||||
import org.springframework.batch.retry.callback.RecoveryRetryCallback;
|
||||
import org.springframework.batch.retry.RetryState;
|
||||
import org.springframework.batch.retry.support.RetryTemplate;
|
||||
|
||||
/**
|
||||
@@ -38,38 +39,34 @@ public class StatefulRetryIntegrationTests {
|
||||
public void testExternalRetryWithFailAndNoRetry() throws Exception {
|
||||
MockRetryCallback callback = new MockRetryCallback();
|
||||
|
||||
RecoveryRetryCallback recoveryCallback = new RecoveryRetryCallback("foo", callback);
|
||||
RetryState retryState = new RetryState("foo");
|
||||
|
||||
RetryTemplate retryTemplate = new RetryTemplate();
|
||||
RecoveryCallbackRetryPolicy retryPolicy = new RecoveryCallbackRetryPolicy(new SimpleRetryPolicy(1));
|
||||
MapRetryContextCache cache = new MapRetryContextCache();
|
||||
retryPolicy.setRetryContextCache(cache);
|
||||
retryTemplate.setRetryPolicy(retryPolicy);
|
||||
retryTemplate.setRetryContextCache(cache);
|
||||
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(1));
|
||||
|
||||
assertFalse(cache.containsKey("foo"));
|
||||
|
||||
Object result = "start_foo";
|
||||
try {
|
||||
result = retryTemplate.execute(recoveryCallback);
|
||||
retryTemplate.execute(callback, retryState);
|
||||
// The first failed attempt we expect to retry...
|
||||
fail("Expected RuntimeException");
|
||||
}
|
||||
catch (RuntimeException e) {
|
||||
assertNull(e.getMessage());
|
||||
assertEquals(null, e.getMessage());
|
||||
}
|
||||
|
||||
assertTrue(cache.containsKey("foo"));
|
||||
|
||||
try {
|
||||
result = retryTemplate.execute(recoveryCallback);
|
||||
// We always get a second attempt...
|
||||
retryTemplate.execute(callback, retryState);
|
||||
// We don't get a second attempt...
|
||||
fail("Expected ExhaustedRetryException");
|
||||
}
|
||||
catch (RuntimeException e) {
|
||||
catch (ExhaustedRetryException e) {
|
||||
// This is now the "exhausted" message:
|
||||
assertNotNull(e.getMessage());
|
||||
// But if template is external we should
|
||||
// swallow the exception when retry is impossible.
|
||||
fail("Did not expect RuntimeException: "+e);
|
||||
}
|
||||
|
||||
assertFalse(cache.containsKey("foo"));
|
||||
@@ -77,26 +74,24 @@ public class StatefulRetryIntegrationTests {
|
||||
// Callback is called once: the recovery path should be called in
|
||||
// handleRetryExhausted (so not in this test)...
|
||||
assertEquals(1, callback.attempts);
|
||||
assertEquals(null, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExternalRetryWithSuccessOnRetry() throws Exception {
|
||||
MockRetryCallback callback = new MockRetryCallback();
|
||||
|
||||
RecoveryRetryCallback recoveryCallback = new RecoveryRetryCallback("foo", callback);
|
||||
RetryState retryState = new RetryState("foo");
|
||||
|
||||
RetryTemplate retryTemplate = new RetryTemplate();
|
||||
RecoveryCallbackRetryPolicy retryPolicy = new RecoveryCallbackRetryPolicy(new SimpleRetryPolicy(2));
|
||||
MapRetryContextCache cache = new MapRetryContextCache();
|
||||
retryPolicy.setRetryContextCache(cache);
|
||||
retryTemplate.setRetryPolicy(retryPolicy);
|
||||
retryTemplate.setRetryContextCache(cache);
|
||||
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(2));
|
||||
|
||||
assertFalse(cache.containsKey("foo"));
|
||||
|
||||
Object result = "start_foo";
|
||||
try {
|
||||
result = retryTemplate.execute(recoveryCallback);
|
||||
result = retryTemplate.execute(callback, retryState);
|
||||
// The first failed attempt we expect to retry...
|
||||
fail("Expected RuntimeException");
|
||||
}
|
||||
@@ -106,7 +101,7 @@ public class StatefulRetryIntegrationTests {
|
||||
|
||||
assertTrue(cache.containsKey("foo"));
|
||||
|
||||
result = retryTemplate.execute(recoveryCallback);
|
||||
result = retryTemplate.execute(callback, retryState);
|
||||
|
||||
assertFalse(cache.containsKey("foo"));
|
||||
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
/*
|
||||
* 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 static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.batch.repeat.RepeatContext;
|
||||
import org.springframework.batch.repeat.context.RepeatContextSupport;
|
||||
import org.springframework.batch.repeat.support.RepeatSynchronizationManager;
|
||||
import org.springframework.batch.retry.ExhaustedRetryException;
|
||||
import org.springframework.batch.retry.RecoveryCallback;
|
||||
import org.springframework.batch.retry.RetryCallback;
|
||||
import org.springframework.batch.retry.RetryContext;
|
||||
import org.springframework.batch.retry.RetryException;
|
||||
import org.springframework.batch.retry.RetryPolicy;
|
||||
import org.springframework.batch.retry.RetryState;
|
||||
import org.springframework.batch.retry.policy.MapRetryContextCache;
|
||||
import org.springframework.batch.retry.policy.NeverRetryPolicy;
|
||||
import org.springframework.batch.retry.policy.SimpleRetryPolicy;
|
||||
|
||||
public class StatefulRecoveryRetryTests {
|
||||
|
||||
private RetryTemplate retryTemplate = new RetryTemplate();
|
||||
|
||||
private int count = 0;
|
||||
|
||||
private List<String> list = new ArrayList<String>();
|
||||
|
||||
@Test
|
||||
public void testOpenSunnyDay() throws Exception {
|
||||
|
||||
final StringHolder item = new StringHolder("foo");
|
||||
RetryCallback writer = new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
count++;
|
||||
list.add(item.string);
|
||||
return item;
|
||||
}
|
||||
};
|
||||
|
||||
RetryContext context = retryTemplate.open(writer, new NeverRetryPolicy(), new RetryState("foo"));
|
||||
assertNotNull(context);
|
||||
// we haven't called the processor yet...
|
||||
assertEquals(0, count);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegisterThrowable() {
|
||||
NeverRetryPolicy retryPolicy = new NeverRetryPolicy();
|
||||
RetryState state = new RetryState("foo");
|
||||
RetryContext context = retryTemplate.open(new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
count++;
|
||||
return null;
|
||||
}
|
||||
}, retryPolicy, state);
|
||||
assertNotNull(context);
|
||||
retryTemplate.registerThrowable(retryPolicy, state, context, new Exception());
|
||||
assertFalse(retryPolicy.canRetry(context));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClose() throws Exception {
|
||||
NeverRetryPolicy retryPolicy = new NeverRetryPolicy();
|
||||
RetryState state = new RetryState("foo");
|
||||
RetryContext context = retryTemplate.open(new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
count++;
|
||||
return null;
|
||||
}
|
||||
}, retryPolicy, state);
|
||||
assertNotNull(context);
|
||||
retryTemplate.registerThrowable(retryPolicy, state, context, new Exception());
|
||||
assertFalse(retryPolicy.canRetry(context));
|
||||
retryTemplate.close(retryPolicy, context, state, true);
|
||||
// still can't retry, even if policy is closed
|
||||
// (not that this would happen in practice)...
|
||||
assertFalse(retryPolicy.canRetry(context));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecoverWithParent() throws Exception {
|
||||
RepeatContext parent = new RepeatContextSupport(null);
|
||||
RepeatSynchronizationManager.register(new RepeatContextSupport(parent));
|
||||
testRecover();
|
||||
assertFalse(parent.isCompleteOnly());
|
||||
RepeatSynchronizationManager.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecover() throws Exception {
|
||||
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(1));
|
||||
final String input = "foo";
|
||||
RetryState state = new RetryState(input);
|
||||
RetryCallback callback = new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
throw new RuntimeException("Barf!");
|
||||
}
|
||||
};
|
||||
RecoveryCallback recoveryCallback = new RecoveryCallback() {
|
||||
public Object recover(RetryContext context) {
|
||||
count++;
|
||||
list.add(input);
|
||||
return input;
|
||||
}
|
||||
};
|
||||
Object result = null;
|
||||
try {
|
||||
result = retryTemplate.execute(callback, recoveryCallback, state);
|
||||
fail("Expected exception on first try");
|
||||
}
|
||||
catch (Exception e) {
|
||||
// expected...
|
||||
}
|
||||
// On the second retry, the recovery path is taken...
|
||||
result = retryTemplate.execute(callback, recoveryCallback, state);
|
||||
assertEquals(input, result); // default result is the item
|
||||
assertEquals(1, count);
|
||||
assertEquals(input, list.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExhaustedClearsHistoryAfterLastAttempt() throws Exception {
|
||||
RetryPolicy retryPolicy = new SimpleRetryPolicy(1);
|
||||
retryTemplate.setRetryPolicy(retryPolicy);
|
||||
|
||||
final String input = "foo";
|
||||
RetryState state = new RetryState(input);
|
||||
RetryCallback callback = new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
throw new RuntimeException("Barf!");
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
retryTemplate.execute(callback, state);
|
||||
fail("Expected ExhaustedRetryException");
|
||||
}
|
||||
catch (RuntimeException e) {
|
||||
assertEquals("Barf!", e.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
retryTemplate.execute(callback, state);
|
||||
fail("Expected ExhaustedRetryException");
|
||||
}
|
||||
catch (ExhaustedRetryException e) {
|
||||
// expected
|
||||
}
|
||||
|
||||
RetryContext context = retryTemplate.open(callback, retryPolicy, state);
|
||||
// True after exhausted - the history is reset...
|
||||
assertTrue(retryPolicy.canRetry(context));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeyGeneratorNotConsistentAfterFailure() throws Throwable {
|
||||
|
||||
RetryPolicy retryPolicy = new SimpleRetryPolicy(3);
|
||||
retryTemplate.setRetryPolicy(retryPolicy);
|
||||
final StringHolder item = new StringHolder("bar");
|
||||
RetryState state = new RetryState(item);
|
||||
|
||||
RetryCallback callback = new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
// This simulates what happens if someone uses a primary key
|
||||
// for hashCode and equals and then relies on default key
|
||||
// generator
|
||||
((StringHolder) item).string = ((StringHolder) item).string + (count++);
|
||||
throw new RuntimeException("Barf!");
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
// TODO: test this
|
||||
// retryTemplate.execute(callback, state);
|
||||
// fail("Expected RetryException");
|
||||
}
|
||||
catch (RetryException ex) {
|
||||
String message = ex.getMessage();
|
||||
assertTrue("Message doesn't contain 'inconsistent': " + message, message.indexOf("inconsistent") >= 0);
|
||||
}
|
||||
|
||||
RetryContext context = retryTemplate.open(callback, retryPolicy, state);
|
||||
// True after exhausted - the history is reset...
|
||||
assertEquals(0, context.getRetryCount());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCacheCapacity() throws Exception {
|
||||
|
||||
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(1));
|
||||
retryTemplate.setRetryContextCache(new MapRetryContextCache(1));
|
||||
|
||||
RetryCallback callback = new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
count++;
|
||||
throw new RuntimeException("Barf!");
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
retryTemplate.execute(callback, new RetryState("foo"));
|
||||
fail("Expected RuntimeException");
|
||||
}
|
||||
catch (RuntimeException e) {
|
||||
assertEquals("Barf!", e.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
retryTemplate.execute(callback, new RetryState("bar"));
|
||||
fail("Expected RetryException");
|
||||
}
|
||||
catch (RetryException e) {
|
||||
String message = e.getMessage();
|
||||
assertTrue("Message does not contain 'capacity': " + message, message.indexOf("capacity") >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCacheCapacityNotReachedIfRecovered() throws Exception {
|
||||
|
||||
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(1);
|
||||
retryTemplate.setRetryPolicy(retryPolicy);
|
||||
retryTemplate.setRetryContextCache(new MapRetryContextCache(2));
|
||||
final StringHolder item = new StringHolder("foo");
|
||||
RetryState state = new RetryState(item);
|
||||
|
||||
RetryCallback callback = new RetryCallback() {
|
||||
public Object doWithRetry(RetryContext context) throws Exception {
|
||||
count++;
|
||||
throw new RuntimeException("Barf!");
|
||||
}
|
||||
};
|
||||
RecoveryCallback recoveryCallback = new RecoveryCallback() {
|
||||
public Object recover(RetryContext context) throws Exception {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
retryTemplate.execute(callback, recoveryCallback, state);
|
||||
fail("Expected RuntimeException");
|
||||
}
|
||||
catch (RuntimeException e) {
|
||||
assertEquals("Barf!", e.getMessage());
|
||||
}
|
||||
retryTemplate.execute(callback, recoveryCallback, state);
|
||||
|
||||
RetryContext context = retryTemplate.open(callback, retryPolicy, state);
|
||||
// True after exhausted - the history is reset...
|
||||
assertEquals(0, context.getRetryCount());
|
||||
|
||||
}
|
||||
|
||||
private static class StringHolder {
|
||||
|
||||
private String string;
|
||||
|
||||
/**
|
||||
* @param string
|
||||
*/
|
||||
public StringHolder(String string) {
|
||||
this.string = string;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
*/
|
||||
public boolean equals(Object obj) {
|
||||
return string.equals(((StringHolder) obj).string);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see java.lang.Object#hashCode()
|
||||
*/
|
||||
public int hashCode() {
|
||||
return string.hashCode();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
public String toString() {
|
||||
return "String: " + string + " (hash = " + hashCode() + ")";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package org.springframework.batch.sample.common;
|
||||
|
||||
import org.springframework.batch.item.ClearFailedException;
|
||||
import org.springframework.batch.item.FlushFailedException;
|
||||
import org.springframework.batch.item.ItemWriter;
|
||||
|
||||
public class CompositeItemWriter<T> implements ItemWriter<T> {
|
||||
|
||||
ItemWriter<T> itemWriter;
|
||||
|
||||
public CompositeItemWriter(ItemWriter<T> itemWriter) {
|
||||
this.itemWriter = itemWriter;
|
||||
}
|
||||
|
||||
public void write(T item) throws Exception {
|
||||
|
||||
//Add business logic here
|
||||
|
||||
itemWriter.write(item);
|
||||
}
|
||||
|
||||
public void clear() throws ClearFailedException {
|
||||
itemWriter.clear();
|
||||
}
|
||||
|
||||
public void flush() throws FlushFailedException {
|
||||
itemWriter.flush();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user