From e8bde07a73df26d702233481bb2e3577c9de40ac Mon Sep 17 00:00:00 2001 From: Michael Minella Date: Fri, 28 Dec 2012 09:06:37 -0600 Subject: [PATCH] BATCH-1714: Added ChunkListener#afterFailedChunk(ChunkContext context) --- dictionary.txt | 1 + .../batch/core/ChunkListener.java | 27 +++++++++++-- .../core/annotation/AfterChunkError.java | 40 +++++++++++++++++++ .../core/listener/ChunkListenerSupport.java | 9 +++++ .../core/listener/CompositeChunkListener.java | 9 +++++ .../listener/MulticasterBatchListener.java | 10 +++++ .../core/listener/StepListenerMetaData.java | 5 ++- .../core/listener/StepListenerSupport.java | 5 +++ .../builder/FaultTolerantStepBuilder.java | 11 ++++- .../batch/core/step/tasklet/TaskletStep.java | 4 +- .../configuration/xml/spring-batch-2.1.xsd | 1 + .../listener/CompositeChunkListenerTests.java | 28 +++++++++---- .../StepListenerFactoryBeanTests.java | 24 ++++++++--- ...tTolerantStepFactoryBeanRollbackTests.java | 24 +++++++---- .../FaultTolerantStepFactoryBeanTests.java | 5 ++- .../step/item/SimpleStepFactoryBeanTests.java | 27 +++++++++++-- 16 files changed, 196 insertions(+), 34 deletions(-) create mode 100644 spring-batch-core/src/main/java/org/springframework/batch/core/annotation/AfterChunkError.java diff --git a/dictionary.txt b/dictionary.txt index 90d2baced..78553c4fb 100644 --- a/dictionary.txt +++ b/dictionary.txt @@ -94,3 +94,4 @@ payloads unflushed memento michael +minella diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/ChunkListener.java b/spring-batch-core/src/main/java/org/springframework/batch/core/ChunkListener.java index 0aa5c74c5..c314d22a1 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/ChunkListener.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/ChunkListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2006-2008 the original author or authors. + * Copyright 2006-2013 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. @@ -15,23 +15,42 @@ */ package org.springframework.batch.core; +import org.springframework.batch.core.scope.context.ChunkContext; + /** * Listener interface for the lifecycle of a chunk. A chunk - * can be through of as a collection of items that will be + * can be through of as a collection of items that will be * committed together. - * + * * @author Lucas Ward + * @author Michael Minella * */ public interface ChunkListener extends StepListener { + static final String ROLLBACK_EXCEPTION_KEY = "sb_rollback_exception"; + /** * Callback before the chunk is executed, but inside the transaction. */ void beforeChunk(); - + /** * Callback after the chunk is executed, outside the transaction. */ void afterChunk(); + + /** + * Callback after a chunk has been marked for rollback. It is invoked + * after transaction rollback. While the rollback will have occurred, + * transactional resources might still be active and accessible. Due to + * this, data access code within this callback will still "participate" in + * the original transaction unless it declares that it run in its own + * transaction. Hence: Use PROPAGATION_REQUIRES_NEW for any + * transactional operation that is called from here. + * + * @param context the chunk context containing the exception that caused + * the underlying rollback. + */ + void afterChunkError(ChunkContext context); } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/annotation/AfterChunkError.java b/spring-batch-core/src/main/java/org/springframework/batch/core/annotation/AfterChunkError.java new file mode 100644 index 000000000..65c92159e --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/annotation/AfterChunkError.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2013 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.core.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.batch.core.ChunkListener; +import org.springframework.batch.core.scope.context.ChunkContext; + +/** + * Marks a method to be called after a has failed and been + * marked for rollback.
+ *
+ * Expected signature: void afterFailedChunk(ChunkContext context) + * + * @author Michael Minella + * @since 2.2 + * @see ChunkListener#afterChunkError(ChunkContext) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface AfterChunkError { + +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/ChunkListenerSupport.java b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/ChunkListenerSupport.java index 8d7979de5..e36fef4e0 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/ChunkListenerSupport.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/ChunkListenerSupport.java @@ -16,6 +16,7 @@ package org.springframework.batch.core.listener; import org.springframework.batch.core.ChunkListener; +import org.springframework.batch.core.scope.context.ChunkContext; /** * Basic support implementation of {@link ChunkListener} @@ -39,4 +40,12 @@ public class ChunkListenerSupport implements ChunkListener { public void beforeChunk() { } + + @Override + /* (non-Javadoc) + * @see org.springframework.batch.core.domain.ChunkListener#afterChunkError(ChunkContext) + */ + public void afterChunkError(ChunkContext context) { + } + } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/CompositeChunkListener.java b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/CompositeChunkListener.java index 2109fba0b..0207b717c 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/CompositeChunkListener.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/CompositeChunkListener.java @@ -19,6 +19,7 @@ import java.util.Iterator; import java.util.List; import org.springframework.batch.core.ChunkListener; +import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.core.Ordered; /** @@ -73,4 +74,12 @@ public class CompositeChunkListener implements ChunkListener { listener.beforeChunk(); } } + + @Override + public void afterChunkError(ChunkContext context) { + for (Iterator iterator = listeners.iterator(); iterator.hasNext();) { + ChunkListener listener = iterator.next(); + listener.afterChunkError(context); + } + } } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/MulticasterBatchListener.java b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/MulticasterBatchListener.java index 83eb33171..24b496ee5 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/MulticasterBatchListener.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/MulticasterBatchListener.java @@ -26,6 +26,7 @@ import org.springframework.batch.core.SkipListener; import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.StepExecutionListener; import org.springframework.batch.core.StepListener; +import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.item.ItemStream; /** @@ -317,4 +318,13 @@ ItemProcessListener, ItemWriteListener, SkipListener { skipListener.onSkipInProcess(item, t); } + @Override + public void afterChunkError(ChunkContext context) { + try { + chunkListener.afterChunkError(context); + } + catch (RuntimeException e) { + throw new StepListenerFailedException("Error in afterFailedChunk.", e); + } + } } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/StepListenerMetaData.java b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/StepListenerMetaData.java index 50a23c371..21f85eaff 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/StepListenerMetaData.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/StepListenerMetaData.java @@ -29,6 +29,7 @@ import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.StepExecutionListener; import org.springframework.batch.core.StepListener; import org.springframework.batch.core.annotation.AfterChunk; +import org.springframework.batch.core.annotation.AfterChunkError; import org.springframework.batch.core.annotation.AfterProcess; import org.springframework.batch.core.annotation.AfterRead; import org.springframework.batch.core.annotation.AfterStep; @@ -44,6 +45,7 @@ import org.springframework.batch.core.annotation.OnSkipInProcess; import org.springframework.batch.core.annotation.OnSkipInRead; import org.springframework.batch.core.annotation.OnSkipInWrite; import org.springframework.batch.core.annotation.OnWriteError; +import org.springframework.batch.core.scope.context.ChunkContext; /** * Enumeration for {@link StepListener} meta data, which ties together the names @@ -59,6 +61,7 @@ public enum StepListenerMetaData implements ListenerMetaData { AFTER_STEP("afterStep", "after-step-method", AfterStep.class, StepExecutionListener.class, StepExecution.class), BEFORE_CHUNK("beforeChunk", "before-chunk-method", BeforeChunk.class, ChunkListener.class), AFTER_CHUNK("afterChunk", "after-chunk-method", AfterChunk.class, ChunkListener.class), + AFTER_CHUNK_ERROR("afterChunkError", "after-chunk-error-method", AfterChunkError.class, ChunkListener.class, ChunkContext.class), BEFORE_READ("beforeRead", "before-read-method", BeforeRead.class, ItemReadListener.class), AFTER_READ("afterRead", "after-read-method", AfterRead.class, ItemReadListener.class, Object.class), ON_READ_ERROR("onReadError", "on-read-error-method", OnReadError.class, ItemReadListener.class, Exception.class), @@ -138,7 +141,7 @@ public enum StepListenerMetaData implements ListenerMetaData { } public static ListenerMetaData[] taskletListenerMetaData() { - return new ListenerMetaData[] {BEFORE_CHUNK, AFTER_CHUNK}; + return new ListenerMetaData[] {BEFORE_CHUNK, AFTER_CHUNK, AFTER_CHUNK_ERROR}; } } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/StepListenerSupport.java b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/StepListenerSupport.java index 70d9d4028..e401b7fb9 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/StepListenerSupport.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/StepListenerSupport.java @@ -26,6 +26,7 @@ import org.springframework.batch.core.SkipListener; import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.StepExecutionListener; import org.springframework.batch.core.StepListener; +import org.springframework.batch.core.scope.context.ChunkContext; /** * Basic no-op implementations of all {@link StepListener} interfaces. @@ -149,4 +150,8 @@ ItemReadListener, ItemProcessListener, ItemWriteListener, SkipListene public void onSkipInWrite(S item, Throwable t) { } + @Override + public void afterChunkError(ChunkContext context) { + } + } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java index 2e0dec69c..1d1bad01f 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java @@ -29,6 +29,7 @@ import org.springframework.batch.core.JobInterruptedException; import org.springframework.batch.core.SkipListener; import org.springframework.batch.core.StepListener; import org.springframework.batch.core.listener.StepListenerFactoryBean; +import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.core.step.FatalStepExecutionException; import org.springframework.batch.core.step.item.BatchRetryTemplate; import org.springframework.batch.core.step.item.ChunkMonitor; @@ -667,6 +668,14 @@ public class FaultTolerantStepBuilder extends SimpleStepBuilder { } } + @Override + public void afterChunkError(ChunkContext context) { + try { + chunkListener.afterChunkError(context); + } + catch (Throwable t) { + throw new FatalStepExecutionException("ChunkListener threw exception, rethrowing as fatal", t); + } + } } - } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/TaskletStep.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/TaskletStep.java index 552cded4d..9d9c07b42 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/TaskletStep.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/TaskletStep.java @@ -70,6 +70,7 @@ import org.springframework.util.Assert; * @author Lucas Ward * @author Ben Hale * @author Robert Kasanicky + * @author Michael Minella */ @SuppressWarnings("serial") public class TaskletStep extends AbstractStep { @@ -351,6 +352,7 @@ public class TaskletStep extends AbstractStep { rollback(stepExecution); } } + chunkListener.afterChunkError(chunkContext); } if (status == TransactionSynchronization.STATUS_UNKNOWN) { logger.error("Rolling back with transaction in unknown state"); @@ -396,10 +398,10 @@ public class TaskletStep extends AbstractStep { } catch (Exception e) { if (transactionAttribute.rollbackOn(e)) { + chunkContext.setAttribute(ChunkListener.ROLLBACK_EXCEPTION_KEY, e); throw e; } } - } finally { diff --git a/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch-2.1.xsd b/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch-2.1.xsd index addde7b81..5bff17301 100644 --- a/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch-2.1.xsd +++ b/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch-2.1.xsd @@ -1069,6 +1069,7 @@ ref" is not required, and only needs to be specified explicitly + diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/listener/CompositeChunkListenerTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/listener/CompositeChunkListenerTests.java index 30f6a8138..19ba25d07 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/listener/CompositeChunkListenerTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/listener/CompositeChunkListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2006-2008 the original author or authors. + * Copyright 2006-2013 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. @@ -15,44 +15,56 @@ */ package org.springframework.batch.core.listener; -import static org.easymock.EasyMock.*; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; import org.junit.Before; import org.junit.Test; import org.springframework.batch.core.ChunkListener; -import org.springframework.batch.core.listener.CompositeChunkListener; +import org.springframework.batch.core.scope.context.ChunkContext; /** * @author Lucas Ward + * @author Michael Minella * */ public class CompositeChunkListenerTests { ChunkListener listener; CompositeChunkListener compositeListener; - + @Before public void setUp() throws Exception { listener = createMock(ChunkListener.class); compositeListener = new CompositeChunkListener(); compositeListener.register(listener); } - + @Test public void testBeforeChunk(){ - + listener.beforeChunk(); replay(listener); compositeListener.beforeChunk(); verify(listener); } - + @Test public void testAfterChunk(){ - + listener.afterChunk(); replay(listener); compositeListener.afterChunk(); verify(listener); } + + @Test + public void testAfterChunkFailed(){ + ChunkContext context = new ChunkContext(null); + listener.afterChunkError(context); + replay(listener); + compositeListener.afterChunkError(context); + verify(listener); + } } diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/listener/StepListenerFactoryBeanTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/listener/StepListenerFactoryBeanTests.java index 3288289ef..7b118078a 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/listener/StepListenerFactoryBeanTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/listener/StepListenerFactoryBeanTests.java @@ -18,7 +18,6 @@ package org.springframework.batch.core.listener; import static junit.framework.Assert.assertTrue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.springframework.batch.core.listener.StepListenerMetaData.AFTER_CHUNK; import static org.springframework.batch.core.listener.StepListenerMetaData.AFTER_STEP; import static org.springframework.batch.core.listener.StepListenerMetaData.AFTER_WRITE; @@ -44,6 +43,8 @@ import org.springframework.batch.core.SkipListener; import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.StepExecutionListener; import org.springframework.batch.core.StepListener; +import org.springframework.batch.core.annotation.AfterChunk; +import org.springframework.batch.core.annotation.AfterChunkError; import org.springframework.batch.core.annotation.AfterProcess; import org.springframework.batch.core.annotation.AfterRead; import org.springframework.batch.core.annotation.AfterStep; @@ -57,6 +58,7 @@ import org.springframework.batch.core.annotation.OnProcessError; import org.springframework.batch.core.annotation.OnReadError; import org.springframework.batch.core.annotation.OnWriteError; import org.springframework.batch.core.configuration.xml.AbstractTestComponent; +import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.Ordered; import org.springframework.util.Assert; @@ -83,10 +85,10 @@ public class StepListenerFactoryBeanTests { public void testStepAndChunk() throws Exception { TestListener testListener = new TestListener(); factoryBean.setDelegate(testListener); - Map metaDataMap = new HashMap(); - metaDataMap.put(AFTER_STEP.getPropertyName(), "destroy"); - metaDataMap.put(AFTER_CHUNK.getPropertyName(), "afterChunk"); - factoryBean.setMetaDataMap(metaDataMap); + // Map metaDataMap = new HashMap(); + // metaDataMap.put(AFTER_STEP.getPropertyName(), "destroy"); + // metaDataMap.put(AFTER_CHUNK.getPropertyName(), "afterChunk"); + // factoryBean.setMetaDataMap(metaDataMap); String readItem = "item"; Integer writeItem = 2; List writeItems = Arrays.asList(writeItem); @@ -95,6 +97,7 @@ public class StepListenerFactoryBeanTests { ((StepExecutionListener) listener).afterStep(stepExecution); ((ChunkListener) listener).beforeChunk(); ((ChunkListener) listener).afterChunk(); + ((ChunkListener) listener).afterChunkError(new ChunkContext(null)); ((ItemReadListener) listener).beforeRead(); ((ItemReadListener) listener).afterRead(readItem); ((ItemReadListener) listener).onReadError(new Exception()); @@ -110,6 +113,7 @@ public class StepListenerFactoryBeanTests { assertTrue(testListener.beforeStepCalled); assertTrue(testListener.beforeChunkCalled); assertTrue(testListener.afterChunkCalled); + assertTrue(testListener.afterChunkErrorCalled); assertTrue(testListener.beforeReadCalled); assertTrue(testListener.afterReadCalled); assertTrue(testListener.onReadErrorCalled); @@ -132,7 +136,6 @@ public class StepListenerFactoryBeanTests { ThreeStepExecutionListener delegate = new ThreeStepExecutionListener(); factoryBean.setDelegate(delegate); Map metaDataMap = new HashMap(); - ; metaDataMap.put(AFTER_STEP.getPropertyName(), "destroy"); factoryBean.setMetaDataMap(metaDataMap); StepListener listener = (StepListener) factoryBean.getObject(); @@ -428,6 +431,8 @@ public class StepListenerFactoryBeanTests { boolean afterChunkCalled = false; + boolean afterChunkErrorCalled = false; + boolean beforeReadCalled = false; boolean afterReadCalled = false; @@ -457,6 +462,7 @@ public class StepListenerFactoryBeanTests { beforeStepCalled = true; } + @AfterStep public void destroy() { afterStepCalled = true; } @@ -466,10 +472,16 @@ public class StepListenerFactoryBeanTests { beforeChunkCalled = true; } + @AfterChunk public void afterChunk() { afterChunkCalled = true; } + @AfterChunkError + public void afterChunkError(ChunkContext context) { + afterChunkErrorCalled = true; + } + @BeforeRead public void beforeReadMethod() { beforeReadCalled = true; diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/FaultTolerantStepFactoryBeanRollbackTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/FaultTolerantStepFactoryBeanRollbackTests.java index bf646e3fb..ae2e803d7 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/FaultTolerantStepFactoryBeanRollbackTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/FaultTolerantStepFactoryBeanRollbackTests.java @@ -29,6 +29,7 @@ import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.StepListener; import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean; +import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.core.step.FatalStepExecutionException; import org.springframework.batch.core.step.factory.FaultTolerantStepFactoryBean; import org.springframework.batch.item.ItemReader; @@ -110,7 +111,7 @@ public class FaultTolerantStepFactoryBeanRollbackTests { @Test public void testBeforeChunkListenerException() throws Exception{ - factory.setListeners(new StepListener []{new ExceptionThrowingChunkListener(true)}); + factory.setListeners(new StepListener []{new ExceptionThrowingChunkListener(1)}); Step step = (Step) factory.getObject(); step.execute(stepExecution); assertEquals(FAILED, stepExecution.getStatus()); @@ -123,7 +124,7 @@ public class FaultTolerantStepFactoryBeanRollbackTests { @Test public void testAfterChunkListenerException() throws Exception{ - factory.setListeners(new StepListener []{new ExceptionThrowingChunkListener(false)}); + factory.setListeners(new StepListener []{new ExceptionThrowingChunkListener(2)}); Step step = (Step) factory.getObject(); step.execute(stepExecution); assertEquals(FAILED, stepExecution.getStatus()); @@ -590,24 +591,31 @@ public class FaultTolerantStepFactoryBeanRollbackTests { class ExceptionThrowingChunkListener implements ChunkListener{ - private boolean throwBefore = true; + private int phase = -1; - public ExceptionThrowingChunkListener(boolean throwBefore) { - this.throwBefore = throwBefore; + public ExceptionThrowingChunkListener(int throwPhase) { + this.phase = throwPhase; } @Override public void beforeChunk() { - if(throwBefore){ + if(phase == 1){ throw new IllegalArgumentException("Planned exception"); } } @Override public void afterChunk() { - throw new IllegalArgumentException("Planned exception"); + if(phase == 2) { + throw new IllegalArgumentException("Planned exception"); + } + } + @Override + public void afterChunkError(ChunkContext context) { + if(phase == 3) { + throw new IllegalArgumentException("Planned exception"); + } } } - } diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/FaultTolerantStepFactoryBeanTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/FaultTolerantStepFactoryBeanTests.java index 84c4c0a02..8a2484751 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/FaultTolerantStepFactoryBeanTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/FaultTolerantStepFactoryBeanTests.java @@ -33,6 +33,7 @@ import org.springframework.batch.core.StepListener; import org.springframework.batch.core.listener.SkipListenerSupport; import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean; +import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.core.step.factory.FaultTolerantStepFactoryBean; import org.springframework.batch.core.step.skip.LimitCheckingItemSkipPolicy; import org.springframework.batch.core.step.skip.SkipLimitExceededException; @@ -806,6 +807,9 @@ public class FaultTolerantStepFactoryBeanTests { listenerCalls.add(5); } + @Override + public void afterChunkError(ChunkContext context) { + } } factory.setItemWriter(new TestItemListenerWriter()); @@ -1109,7 +1113,6 @@ public class FaultTolerantStepFactoryBeanTests { @SuppressWarnings("serial") public static class NonExistentException extends Exception { - } } diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/SimpleStepFactoryBeanTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/SimpleStepFactoryBeanTests.java index d362bff4f..2e6e4fd99 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/SimpleStepFactoryBeanTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/SimpleStepFactoryBeanTests.java @@ -46,6 +46,7 @@ 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.scope.context.ChunkContext; import org.springframework.batch.core.step.AbstractStep; import org.springframework.batch.core.step.factory.SimpleStepFactoryBean; import org.springframework.batch.item.ItemProcessor; @@ -217,7 +218,7 @@ public class SimpleStepFactoryBeanTests { @Test public void testChunkListeners() throws Exception { - String[] items = new String[] { "1", "2", "3", "4", "5", "6", "7" }; + String[] items = new String[] { "1", "2", "3", "4", "5", "6", "7", "error" }; int commitInterval = 3; SimpleStepFactoryBean factory = getStepFactory(items); @@ -227,6 +228,10 @@ public class SimpleStepFactoryBeanTests { @Override public void beforeWrite(List items) { + if(items.contains("error")) { + throw new RuntimeException("rollback the last chunk"); + } + trail = trail + "2"; } @@ -241,6 +246,8 @@ public class SimpleStepFactoryBeanTests { int afterCount = 0; + int failedCount = 0; + private AssertingWriteListener writeListener; public CountingChunkListener(AssertingWriteListener writeListener) { @@ -259,6 +266,12 @@ public class SimpleStepFactoryBeanTests { writeListener.trail = writeListener.trail + "1"; beforeCount++; } + + @Override + public void afterChunkError(ChunkContext context) { + writeListener.trail = writeListener.trail + "5"; + failedCount++; + } } AssertingWriteListener writeListener = new AssertingWriteListener(); CountingChunkListener chunkListener = new CountingChunkListener(writeListener); @@ -272,13 +285,15 @@ public class SimpleStepFactoryBeanTests { JobExecution jobExecution = repository.createJobExecution(job.getName(), new JobParameters()); job.execute(jobExecution); - assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus()); + assertEquals(BatchStatus.FAILED, jobExecution.getStatus()); assertNull(reader.read()); - assertEquals(items.length, written.size()); + assertEquals(6, written.size()); int expectedListenerCallCount = (items.length / commitInterval) + 1; - assertEquals(expectedListenerCallCount, chunkListener.afterCount); + assertEquals(expectedListenerCallCount - 1, chunkListener.afterCount); assertEquals(expectedListenerCallCount, chunkListener.beforeCount); + assertEquals(1, chunkListener.failedCount); + assertEquals("1234123415", writeListener.trail); assertTrue("Listener order not as expected: " + writeListener.trail, writeListener.trail.startsWith("1234")); } @@ -392,6 +407,10 @@ public class SimpleStepFactoryBeanTests { public void beforeChunk() { } + @Override + public void afterChunkError(ChunkContext context) { + } + } TestItemListenerWriter itemWriter = new TestItemListenerWriter();