From b050f95f03cd0e49193f594732dd520f2b22bbfc Mon Sep 17 00:00:00 2001 From: dsyer Date: Tue, 1 Apr 2008 20:09:51 +0000 Subject: [PATCH] RESOLVED - issue BATCH-537: Bad ItemKeyGenerator strategy can lead to infinite loop in retry Some changes to account for retry exceptions and force a failure if strange hashCode/equals are detected. --- .../item/SimpleRetryExceptionHandler.java | 14 +- .../step/item/SkipLimitStepFactoryBean.java | 18 ++- .../item/StatefulRetryStepFactoryBean.java | 5 +- .../SimpleRetryExceptionHandlerTests.java | 46 ++++-- .../StatefulRetryStepFactoryBeanTests.java | 4 +- .../retry/policy/ItemWriterRetryPolicy.java | 41 ++++-- .../retry/policy/MapRetryContextCache.java | 44 +++++- .../RetryCacheCapacityExceededException.java | 52 +++++++ .../batch/retry/policy/RetryContextCache.java | 2 +- .../policy/ItemWriterRetryPolicyTests.java | 134 ++++++++++++++++-- 10 files changed, 316 insertions(+), 44 deletions(-) create mode 100644 spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/RetryCacheCapacityExceededException.java diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/SimpleRetryExceptionHandler.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/SimpleRetryExceptionHandler.java index 27c5c5955..cd14f5955 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/SimpleRetryExceptionHandler.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/SimpleRetryExceptionHandler.java @@ -22,6 +22,7 @@ import org.springframework.batch.retry.RetryCallback; import org.springframework.batch.retry.RetryContext; import org.springframework.batch.retry.RetryPolicy; import org.springframework.batch.retry.listener.RetryListenerSupport; +import org.springframework.batch.support.BinaryExceptionClassifier; /** * @author Dave Syer @@ -34,17 +35,22 @@ public class SimpleRetryExceptionHandler extends RetryListenerSupport implements */ private static final String EXHAUSTED = SimpleRetryExceptionHandler.class.getName() + ".RETRY_EXHAUSTED"; - private RetryPolicy retryPolicy; + final private RetryPolicy retryPolicy; - private ExceptionHandler exceptionHandler; + final private ExceptionHandler exceptionHandler; + + final private BinaryExceptionClassifier fatalExceptionClassifier; /** * @param retryPolicy * @param exceptionHandler + * @param classes */ - public SimpleRetryExceptionHandler(RetryPolicy retryPolicy, ExceptionHandler exceptionHandler) { + public SimpleRetryExceptionHandler(RetryPolicy retryPolicy, ExceptionHandler exceptionHandler, Class[] classes) { this.retryPolicy = retryPolicy; this.exceptionHandler = exceptionHandler; + this.fatalExceptionClassifier = new BinaryExceptionClassifier(); + fatalExceptionClassifier.setExceptionClasses(classes); } /* @@ -55,7 +61,7 @@ public class SimpleRetryExceptionHandler extends RetryListenerSupport implements public void handleException(RepeatContext context, Throwable throwable) throws Throwable { // Only bother to check the delegate exception handler if we know that // retry is exhausted - if (context.hasAttribute(EXHAUSTED)) { + if (!fatalExceptionClassifier.isDefault(throwable) || context.hasAttribute(EXHAUSTED)) { exceptionHandler.handleException(context, throwable); } } 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 0e99d8b9b..463c739c2 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 @@ -111,11 +111,8 @@ public class SkipLimitStepFactoryBean extends SimpleStepFactoryBean { * to absorb exceptions at the step level because the failed items * will never re-appear after a rollback. */ - List fatalExceptionList = new ArrayList(Arrays.asList(fatalExceptionClasses)); - if (!fatalExceptionList.contains(SkipLimitExceededException.class)) { - fatalExceptionList.add(SkipLimitExceededException.class); - } - fatalExceptionClasses = (Class[]) fatalExceptionList.toArray(new Class[0]); + addFatalExceptionIfMissing(SkipLimitExceededException.class); + List fatalExceptionList = Arrays.asList(fatalExceptionClasses); LimitCheckingItemSkipPolicy skipPolicy = new LimitCheckingItemSkipPolicy(skipLimit, Arrays .asList(skippableExceptionClasses), fatalExceptionList); @@ -143,4 +140,15 @@ public class SkipLimitStepFactoryBean extends SimpleStepFactoryBean { step.setItemHandler(itemHandler); } + /** + * @return + */ + public void addFatalExceptionIfMissing(Class cls) { + List fatalExceptionList = new ArrayList(Arrays.asList(fatalExceptionClasses)); + if (!fatalExceptionList.contains(cls)) { + fatalExceptionList.add(cls); + } + fatalExceptionClasses = (Class[]) fatalExceptionList.toArray(new Class[0]); + } + } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/StatefulRetryStepFactoryBean.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/StatefulRetryStepFactoryBean.java index fa2fbb2e5..53f168856 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/StatefulRetryStepFactoryBean.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/StatefulRetryStepFactoryBean.java @@ -22,6 +22,7 @@ import org.springframework.batch.item.ItemKeyGenerator; import org.springframework.batch.item.ItemReader; import org.springframework.batch.item.ItemRecoverer; import org.springframework.batch.item.ItemWriter; +import org.springframework.batch.retry.RetryException; import org.springframework.batch.retry.RetryListener; import org.springframework.batch.retry.RetryOperations; import org.springframework.batch.retry.RetryPolicy; @@ -116,6 +117,8 @@ public class StatefulRetryStepFactoryBean extends SkipLimitStepFactoryBean { if (retryLimit > 0) { + addFatalExceptionIfMissing(RetryException.class); + SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(retryLimit); if (retryableExceptionClasses != null) { retryPolicy.setRetryableExceptionClasses(retryableExceptionClasses); @@ -124,7 +127,7 @@ public class StatefulRetryStepFactoryBean extends SkipLimitStepFactoryBean { // Co-ordinate the retry policy with the exception handler: getStepOperations() - .setExceptionHandler(new SimpleRetryExceptionHandler(retryPolicy, getExceptionHandler())); + .setExceptionHandler(new SimpleRetryExceptionHandler(retryPolicy, getExceptionHandler(), getFatalExceptionClasses())); ItemWriterRetryPolicy itemProviderRetryPolicy = new ItemWriterRetryPolicy(retryPolicy); diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/SimpleRetryExceptionHandlerTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/SimpleRetryExceptionHandlerTests.java index 42d5406ab..3efaaf968 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/SimpleRetryExceptionHandlerTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/SimpleRetryExceptionHandlerTests.java @@ -42,8 +42,9 @@ public class SimpleRetryExceptionHandlerTests extends TestCase { protected void setUp() throws Exception { RepeatSynchronizationManager.register(context); } - - /* (non-Javadoc) + + /* + * (non-Javadoc) * @see junit.framework.TestCase#tearDown() */ protected void tearDown() throws Exception { @@ -59,7 +60,7 @@ public class SimpleRetryExceptionHandlerTests extends TestCase { RetryPolicy retryPolicy = new NeverRetryPolicy(); RuntimeException ex = new RuntimeException("foo"); - SimpleRetryExceptionHandler handler = getHandlerAfterRetry(retryPolicy, ex); + SimpleRetryExceptionHandler handler = getHandlerAfterRetry(retryPolicy, ex, new Class[] { Error.class }); // Then pretend to handle the exception in the parent context... try { @@ -71,7 +72,8 @@ public class SimpleRetryExceptionHandlerTests extends TestCase { } assertEquals(0, context.attributeNames().length); - // One for the retry exhausted flag and one for the counter in the delegate exception handler + // One for the retry exhausted flag and one for the counter in the + // delegate exception handler assertEquals(2, context.getParent().attributeNames().length); } @@ -84,24 +86,50 @@ public class SimpleRetryExceptionHandlerTests extends TestCase { RetryPolicy retryPolicy = new AlwaysRetryPolicy(); RuntimeException ex = new RuntimeException("foo"); - SimpleRetryExceptionHandler handler = getHandlerAfterRetry(retryPolicy, ex); + SimpleRetryExceptionHandler handler = getHandlerAfterRetry(retryPolicy, ex, new Class[] { Error.class }); // Then pretend to handle the exception in the parent context... handler.handleException(context.getParent(), ex); - + assertEquals(0, context.attributeNames().length); assertEquals(0, context.getParent().attributeNames().length); } + /** + * Test method for + * {@link org.springframework.batch.core.step.item.SimpleRetryExceptionHandler#handleException(org.springframework.batch.repeat.RepeatContext, java.lang.Throwable)}. + */ + public void testRethrowWhenFatal() throws Throwable { + + RetryPolicy retryPolicy = new AlwaysRetryPolicy(); + RuntimeException ex = new RuntimeException("foo"); + + SimpleRetryExceptionHandler handler = getHandlerAfterRetry(retryPolicy, ex, new Class[] { RuntimeException.class }); + + // Then pretend to handle the exception in the parent context... + try { + handler.handleException(context.getParent(), ex); + fail("Expected RuntimeException"); + } + catch (RuntimeException e) { + assertEquals(ex, e); + } + + assertEquals(0, context.attributeNames().length); + // One for the counter in the delegate exception handler + assertEquals(1, context.getParent().attributeNames().length); + } + /** * @param retryPolicy * @param ex * @return */ - private SimpleRetryExceptionHandler getHandlerAfterRetry(RetryPolicy retryPolicy, RuntimeException ex) { - + private SimpleRetryExceptionHandler getHandlerAfterRetry(RetryPolicy retryPolicy, RuntimeException ex, Class[] fatalExceptions) { + // Always rethrow if the retry is exhausted - SimpleRetryExceptionHandler handler = new SimpleRetryExceptionHandler(retryPolicy, new SimpleLimitExceptionHandler(0)); + SimpleRetryExceptionHandler handler = new SimpleRetryExceptionHandler(retryPolicy, + new SimpleLimitExceptionHandler(0), fatalExceptions); // Simulate a failed retry... RetryContext retryContext = retryPolicy.open(null, null); diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/StatefulRetryStepFactoryBeanTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/StatefulRetryStepFactoryBeanTests.java index d5d77e206..1995ca6ef 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/StatefulRetryStepFactoryBeanTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/StatefulRetryStepFactoryBeanTests.java @@ -32,8 +32,6 @@ import org.springframework.batch.core.repository.dao.MapJobExecutionDao; import org.springframework.batch.core.repository.dao.MapJobInstanceDao; import org.springframework.batch.core.repository.dao.MapStepExecutionDao; import org.springframework.batch.core.repository.support.SimpleJobRepository; -import org.springframework.batch.core.step.item.ItemOrientedStep; -import org.springframework.batch.core.step.item.StatefulRetryStepFactoryBean; import org.springframework.batch.item.AbstractItemWriter; import org.springframework.batch.item.ItemReader; import org.springframework.batch.item.ItemRecoverer; @@ -125,5 +123,5 @@ public class StatefulRetryStepFactoryBeanTests extends TestCase { step.execute(new StepExecution(step, jobExecution)); } - + } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/ItemWriterRetryPolicy.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/ItemWriterRetryPolicy.java index a8ac02046..273df78e7 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/ItemWriterRetryPolicy.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/ItemWriterRetryPolicy.java @@ -24,6 +24,7 @@ import org.springframework.batch.item.ItemRecoverer; import org.springframework.batch.repeat.support.RepeatSynchronizationManager; 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.ItemWriterRetryCallback; @@ -136,6 +137,10 @@ public class ItemWriterRetryPolicy extends AbstractStatefulRetryPolicy { final private Object item; + final private Object key; + + final private int initialHashCode; + // The delegate context... private RetryContext delegateContext; @@ -147,10 +152,12 @@ public class ItemWriterRetryPolicy extends AbstractStatefulRetryPolicy { public ItemWriterRetryContext(ItemWriterRetryCallback callback, RetryContext parent) { super(parent); - this.item = callback.getItem(); this.recoverer = callback.getRecoverer(); this.keyGenerator = callback.getKeyGenerator(); + this.item = callback.getItem(); + this.key = keyGenerator.getKey(item); this.failedItemIdentifier = callback.getFailedItemIdentifier(); + this.initialHashCode = key.hashCode(); } public boolean canRetry(RetryContext context) { @@ -162,10 +169,15 @@ public class ItemWriterRetryPolicy extends AbstractStatefulRetryPolicy { } public RetryContext open(RetryCallback callback, RetryContext parent) { - if (hasFailed(failedItemIdentifier, keyGenerator, item)) { - this.delegateContext = retryContextCache.get(keyGenerator.getKey(item)); + if (hasFailed(failedItemIdentifier, 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"); + } } - if (this.delegateContext == null) { + else { // Only create a new context if we don't know the history of // this item: this.delegateContext = delegate.open(callback, null); @@ -175,7 +187,14 @@ public class ItemWriterRetryPolicy extends AbstractStatefulRetryPolicy { } public void registerThrowable(RetryContext context, Throwable throwable) throws TerminatedRetryException { - retryContextCache.put(keyGenerator.getKey(item), this.delegateContext); + // 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); } @@ -191,7 +210,7 @@ public class ItemWriterRetryPolicy extends AbstractStatefulRetryPolicy { public Object handleRetryExhausted(RetryContext context) throws Exception { // If there is no going back, then we can remove the history - retryContextCache.remove(keyGenerator.getKey(item)); + retryContextCache.remove(key); RepeatSynchronizationManager.setCompleteOnly(); if (recoverer != null) { boolean success = recoverer.recover(item, context.getLastThrowable()); @@ -225,15 +244,13 @@ public class ItemWriterRetryPolicy extends AbstractStatefulRetryPolicy { * item key. * * @param failedItemIdentifier - * @param keyGenerator - * @param item + * @param key * @return */ - protected boolean hasFailed(FailedItemIdentifier failedItemIdentifier, ItemKeyGenerator keyGenerator, Object item) { + protected boolean hasFailed(FailedItemIdentifier failedItemIdentifier, Object key) { if (failedItemIdentifier != null) { - return failedItemIdentifier.hasFailed(item); + return failedItemIdentifier.hasFailed(key); } - return retryContextCache.containsKey(keyGenerator.getKey(item)); + return retryContextCache.containsKey(key); } - } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/MapRetryContextCache.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/MapRetryContextCache.java index 06ce25c11..e4c039b98 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/MapRetryContextCache.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/MapRetryContextCache.java @@ -24,15 +24,52 @@ import org.springframework.batch.retry.RetryContext; /** * Map-based implementation of {@link RetryContextCache}. The map backing the - * cache of contexts is sytchronized. + * cache of contexts is synchronized. * * @author Dave Syer * */ public class MapRetryContextCache implements RetryContextCache { + /** + * Default value for maximum capacity of the cache. This is set to a + * reasonably low value (4096) to avoid users inadvertently filling the + * cache with item keys that are inconsistent. + */ + public static final int DEFAULT_CAPACITY = 4096; + private Map map = Collections.synchronizedMap(new HashMap()); + private int capacity; + + /** + * Create a {@link MapRetryContextCache} with default capacity. + */ + public MapRetryContextCache() { + this(DEFAULT_CAPACITY); + } + + /** + * @param defaultCapacity + */ + public MapRetryContextCache(int defaultCapacity) { + super(); + this.capacity = defaultCapacity; + } + + /** + * Public setter for the capacity. Prevents the cache from growing + * unboundedly if items that fail are misidentified and two references to an + * identical item actually do not have the same key. This can happen when + * users implement equals and hashCode based on mutable fields, for + * instance. + * + * @param capacity the capacity to set + */ + public void setCapacity(int capacity) { + this.capacity = capacity; + } + public boolean containsKey(Object key) { return map.containsKey(key); } @@ -42,6 +79,11 @@ public class MapRetryContextCache implements RetryContextCache { } public void put(Object key, RetryContext context) { + if (map.size() >= capacity) { + throw new RetryCacheCapacityExceededException("Retry cache capacity limit breached. " + + "Do you need to re-consider the implementation of the key generator, " + + "or the equals and hashCode of the items that failed?"); + } map.put(key, context); } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/RetryCacheCapacityExceededException.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/RetryCacheCapacityExceededException.java new file mode 100644 index 000000000..2bc264eb6 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/RetryCacheCapacityExceededException.java @@ -0,0 +1,52 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.batch.retry.policy; + +import org.springframework.batch.retry.RetryException; + +/** + * Exception that indicates that a cache limit was exceeded. This is often a + * sign of badly or inconsistently implemented hashCode, equals in failed items. + * Items can then fail repeatedly and appear different to the cache, so they get + * added over and over again until a limit is reached and this exception is + * thrown. Consult the documentation of the {@link RetryContextCache} in use to + * determine how to increase the limit if appropriate. + * + * @author Dave Syer + * + */ +public class RetryCacheCapacityExceededException extends RetryException { + + /** + * Constructs a new instance with a message. + * + * @param message + */ + public RetryCacheCapacityExceededException(String message) { + super(message); + } + + /** + * Constructs a new instance with a message and nested exception. + * + * @param msg the exception message. + * + */ + public RetryCacheCapacityExceededException(String msg, Throwable nested) { + super(msg, nested); + } + +} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/RetryContextCache.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/RetryContextCache.java index b67bf8f20..ceb21c7b7 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/RetryContextCache.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/policy/RetryContextCache.java @@ -31,7 +31,7 @@ public interface RetryContextCache { RetryContext get(Object key); - void put(Object key, RetryContext context); + void put(Object key, RetryContext context) throws RetryCacheCapacityExceededException; void remove(Object key); diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/retry/policy/ItemWriterRetryPolicyTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/retry/policy/ItemWriterRetryPolicyTests.java index 7fa1af0cb..6e09a0518 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/retry/policy/ItemWriterRetryPolicyTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/retry/policy/ItemWriterRetryPolicyTests.java @@ -31,6 +31,7 @@ import org.springframework.batch.repeat.context.RepeatContextSupport; import org.springframework.batch.repeat.support.RepeatSynchronizationManager; 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.callback.ItemWriterRetryCallback; import org.springframework.batch.retry.context.RetryContextSupport; @@ -46,12 +47,6 @@ public class ItemWriterRetryPolicyTests extends TestCase { private List list = new ArrayList(); - private ItemKeyGenerator keyGenerator = new ItemKeyGenerator() { - public Object getKey(Object item) { - return item; - } - }; - protected void setUp() throws Exception { super.setUp(); // The list simulates a failed delivery, redelivery of the same message, @@ -294,10 +289,96 @@ public class ItemWriterRetryPolicyTests extends TestCase { MapRetryContextCache cache = new MapRetryContextCache(); policy.setRetryContextCache(cache); cache.put("foo", new RetryContextSupport(null)); - assertTrue(policy.hasFailed(null, keyGenerator , "foo")); + assertTrue(policy.hasFailed(null, "foo")); } - private static class MockFailedItemProvider extends ListItemReader implements ItemKeyGenerator, FailedItemIdentifier { + public void testKeyGeneratorNotConsistentAfterFailure() throws Throwable { + + AbstractItemWriter writer = new AbstractItemWriter() { + public void write(Object data) { + // This simulates what happens if someone uses a primary key + // for hasCode and equals and then relies on default key + // generator + ((StringHolder) data).string = ((StringHolder) data).string + (count++); + throw new RuntimeException("Barf!"); + } + }; + + policy = new ItemWriterRetryPolicy(); + policy.setDelegate(new SimpleRetryPolicy(3)); + StringHolder item = new StringHolder("bar"); + ItemWriterRetryCallback callback = new ItemWriterRetryCallback(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 ItemWriterRetryPolicy(); + policy.setDelegate(new SimpleRetryPolicy(1)); + policy.setRetryContextCache(new MapRetryContextCache(1)); + AbstractItemWriter writer = new AbstractItemWriter() { + public void write(Object data) { + count++; + list.add(data); + } + }; + RetryContext context; + context = policy.open(new ItemWriterRetryCallback("foo", writer), null); + policy.registerThrowable(context, null); + assertEquals(0, context.getRetryCount()); + context = policy.open(new ItemWriterRetryCallback("bar", 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 ItemWriterRetryPolicy(); + policy.setDelegate(new SimpleRetryPolicy(1)); + policy.setRetryContextCache(new MapRetryContextCache(2)); + AbstractItemWriter writer = new AbstractItemWriter() { + public void write(Object data) { + count++; + list.add(data); + } + }; + RetryContext context; + context = policy.open(new ItemWriterRetryCallback("foo", writer), null); + policy.registerThrowable(context, null); + assertEquals(0, context.getRetryCount()); + policy.registerThrowable(context, new RuntimeException("foo")); + context = policy.open(new ItemWriterRetryCallback("bar", writer), null); + policy.registerThrowable(context, null); + policy.handleRetryExhausted(context); + context = policy.open(new ItemWriterRetryCallback("spam", writer), null); + policy.registerThrowable(context, null); + assertEquals(0, context.getRetryCount()); + } + + private static class MockFailedItemProvider extends ListItemReader implements ItemKeyGenerator, + FailedItemIdentifier { private int hasFailedCount = 0; @@ -316,4 +397,41 @@ public class ItemWriterRetryPolicyTests extends TestCase { } + 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() + ")"; + } + + } + }