diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/SkipLimitStepFactoryBean.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/SkipLimitStepFactoryBean.java index 25e8353d3..181d81493 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/SkipLimitStepFactoryBean.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/SkipLimitStepFactoryBean.java @@ -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 extends SimpleStepFactoryBean skippableExceptionClasses.add(Exception.class); } - private ItemKeyGenerator itemKeyGenerator; - private int cacheCapacity = 0; private int retryLimit = 0; @@ -193,17 +189,6 @@ public class SkipLimitStepFactoryBean extends SimpleStepFactoryBean 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 extends SimpleStepFactoryBean 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 extends SimpleStepFactoryBean 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> exceptions = new ArrayList>( skippableExceptionClasses); @@ -282,8 +261,8 @@ public class SkipLimitStepFactoryBean extends SimpleStepFactoryBean ItemSkipPolicy writeSkipPolicy = new LimitCheckingItemSkipPolicy(skipLimit, exceptions, new ArrayList>(fatalExceptionClasses)); StatefulRetryStepHandler itemHandler = new StatefulRetryStepHandler(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 extends SimpleStepFactoryBean 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 extends SimpleStepFactoryBean * @param itemReader * @param itemWriter * @param retryTemplate - * @param itemKeyGenerator */ public StatefulRetryStepHandler(ItemReader itemReader, ItemProcessor itemProcessor, ItemWriter 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 extends SimpleStepFactoryBean */ private void retryChunk(final Chunk 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 extends SimpleStepFactoryBean } } - }); + }; 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 diff --git a/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/container/jms/BatchMessageListenerContainerIntegrationTests.java b/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/container/jms/BatchMessageListenerContainerIntegrationTests.java index 4e2aa00b0..bc94a7585 100644 --- a/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/container/jms/BatchMessageListenerContainerIntegrationTests.java +++ b/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/container/jms/BatchMessageListenerContainerIntegrationTests.java @@ -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; diff --git a/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/jms/ExternalRetryInBatchTests.java b/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/jms/ExternalRetryInBatchTests.java index aa25d6020..a895ae556 100644 --- a/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/jms/ExternalRetryInBatchTests.java +++ b/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/jms/ExternalRetryInBatchTests.java @@ -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; diff --git a/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/retry/jms/ExternalRetryTests.java b/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/retry/jms/ExternalRetryTests.java index 3c469855a..78c900cb2 100644 --- a/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/retry/jms/ExternalRetryTests.java +++ b/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/retry/jms/ExternalRetryTests.java @@ -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 writer = new ItemWriter() { public void write(final List 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); diff --git a/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/retry/jms/SynchronousTests.java b/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/retry/jms/SynchronousTests.java index e1491bb0a..20a83b2b0 100644 --- a/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/retry/jms/SynchronousTests.java +++ b/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/retry/jms/SynchronousTests.java @@ -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 diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/RetryOperations.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/RetryOperations.java index 7966ae0bb..ab14ab7a1 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/RetryOperations.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/RetryOperations.java @@ -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.

+ * + * 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; + } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/RetryState.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/RetryState.java new file mode 100644 index 000000000..632de9187 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/RetryState.java @@ -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; + } + +} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/callback/RecoveryRetryCallback.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/callback/RecoveryRetryCallback.java deleted file mode 100644 index 49d602256..000000000 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/callback/RecoveryRetryCallback.java +++ /dev/null @@ -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; - } - -} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/interceptor/StatefulRetryOperationsInterceptor.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/interceptor/StatefulRetryOperationsInterceptor.java index d083114ce..4f801150a 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/interceptor/StatefulRetryOperationsInterceptor.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/interceptor/StatefulRetryOperationsInterceptor.java @@ -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 + ")"); diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/AbstractStatefulRetryPolicy.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/AbstractStatefulRetryPolicy.java deleted file mode 100644 index 9e5d819db..000000000 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/AbstractStatefulRetryPolicy.java +++ /dev/null @@ -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> recoverableExceptionClasses = new HashSet>(); - - 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> temp = new HashSet>(); - for (int i = 0; i < retryableExceptionClasses.length; i++) { - addRecoverableExceptionClass(retryableExceptionClasses[i], temp); - } - this.recoverableExceptionClasses = temp; - } - - private void addRecoverableExceptionClass(Class retryableExceptionClass, Set> 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 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; - } - -} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/AbstractStatelessRetryPolicy.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/AbstractStatelessRetryPolicy.java index d25e68ab4..153a38c1b 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/AbstractStatelessRetryPolicy.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/AbstractStatelessRetryPolicy.java @@ -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 { diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/RecoveryCallbackRetryPolicy.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/RecoveryCallbackRetryPolicy.java deleted file mode 100644 index 8d8a032c6..000000000 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/RecoveryCallbackRetryPolicy.java +++ /dev/null @@ -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(); - } - - } - -} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/SimpleRetryPolicy.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/SimpleRetryPolicy.java index 0b547aa2e..f32546fa9 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/SimpleRetryPolicy.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/SimpleRetryPolicy.java @@ -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; } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/support/RetryTemplate.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/support/RetryTemplate.java index 417aad579..fd99a9756 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/support/RetryTemplate.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/support/RetryTemplate.java @@ -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) { diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/support/StatefulRetryTemplate.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/support/StatefulRetryTemplate.java deleted file mode 100644 index 467bec09c..000000000 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/support/StatefulRetryTemplate.java +++ /dev/null @@ -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; - } - -} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/retry/callback/RecoveryRetryCallbackTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/retry/callback/RecoveryRetryCallbackTests.java deleted file mode 100644 index 636a03f3d..000000000 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/retry/callback/RecoveryRetryCallbackTests.java +++ /dev/null @@ -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 calls = new ArrayList(); - - 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()); - } -} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/retry/policy/ExternalRetryPolicyTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/retry/policy/ExternalRetryPolicyTests.java deleted file mode 100644 index ecb5b1560..000000000 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/retry/policy/ExternalRetryPolicyTests.java +++ /dev/null @@ -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++; - } - - } - -} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/retry/policy/FatalExceptionRetryPolicyTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/retry/policy/FatalExceptionRetryPolicyTests.java new file mode 100644 index 000000000..0ce45dcff --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/retry/policy/FatalExceptionRetryPolicyTests.java @@ -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>() { + { + 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>() { + { + 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; + } + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/retry/policy/RecoveryRetryPolicyTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/retry/policy/RecoveryRetryPolicyTests.java deleted file mode 100644 index 029ae96fb..000000000 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/retry/policy/RecoveryRetryPolicyTests.java +++ /dev/null @@ -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 list = new ArrayList(); - - 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() + ")"; - } - - } - -} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/retry/policy/StatefulRetryIntegrationTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/retry/policy/StatefulRetryIntegrationTests.java index 01cec615c..3f4caf8d8 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/retry/policy/StatefulRetryIntegrationTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/retry/policy/StatefulRetryIntegrationTests.java @@ -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")); diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/retry/support/StatefulRecoveryRetryTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/retry/support/StatefulRecoveryRetryTests.java new file mode 100644 index 000000000..8b4b8bd64 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/retry/support/StatefulRecoveryRetryTests.java @@ -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 list = new ArrayList(); + + @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() + ")"; + } + + } + +} diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/sample/common/CompositeItemWriter.java b/spring-batch-samples/src/main/java/org/springframework/batch/sample/common/CompositeItemWriter.java deleted file mode 100644 index 686301f5b..000000000 --- a/spring-batch-samples/src/main/java/org/springframework/batch/sample/common/CompositeItemWriter.java +++ /dev/null @@ -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 implements ItemWriter { - - ItemWriter itemWriter; - - public CompositeItemWriter(ItemWriter 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(); - } -}