BATCH-1714: Added ChunkListener#afterFailedChunk(ChunkContext context)

This commit is contained in:
Michael Minella
2012-12-28 09:06:37 -06:00
parent 4b20665aa5
commit e8bde07a73
16 changed files with 196 additions and 34 deletions

View File

@@ -94,3 +94,4 @@ payloads
unflushed
memento
michael
minella

View File

@@ -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: <em> Use PROPAGATION_REQUIRES_NEW for any
* transactional operation that is called from here.</em>
*
* @param context the chunk context containing the exception that caused
* the underlying rollback.
*/
void afterChunkError(ChunkContext context);
}

View File

@@ -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.<br>
* <br/>
* 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 {
}

View File

@@ -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) {
}
}

View File

@@ -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<ChunkListener> iterator = listeners.iterator(); iterator.hasNext();) {
ChunkListener listener = iterator.next();
listener.afterChunkError(context);
}
}
}

View File

@@ -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<T, S>, ItemWriteListener<S>, SkipListener<T, S> {
skipListener.onSkipInProcess(item, t);
}
@Override
public void afterChunkError(ChunkContext context) {
try {
chunkListener.afterChunkError(context);
}
catch (RuntimeException e) {
throw new StepListenerFailedException("Error in afterFailedChunk.", e);
}
}
}

View File

@@ -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};
}
}

View File

@@ -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<T>, ItemProcessListener<T,S>, ItemWriteListener<S>, SkipListene
public void onSkipInWrite(S item, Throwable t) {
}
@Override
public void afterChunkError(ChunkContext context) {
}
}

View File

@@ -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<I, O> extends SimpleStepBuilder<I, O> {
}
}
@Override
public void afterChunkError(ChunkContext context) {
try {
chunkListener.afterChunkError(context);
}
catch (Throwable t) {
throw new FatalStepExecutionException("ChunkListener threw exception, rethrowing as fatal", t);
}
}
}
}

View File

@@ -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 {

View File

@@ -1069,6 +1069,7 @@ ref" is not required, and only needs to be specified explicitly
<xsd:attribute name="after-step-method" type="xsd:string" />
<xsd:attribute name="before-chunk-method" type="xsd:string" />
<xsd:attribute name="after-chunk-method" type="xsd:string" />
<xsd:attribute name="after-chunk-error-method" type="xsd:string" />
<xsd:attribute name="before-read-method" type="xsd:string" />
<xsd:attribute name="after-read-method" type="xsd:string" />
<xsd:attribute name="on-read-error-method" type="xsd:string" />

View File

@@ -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);
}
}

View File

@@ -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<String, String> metaDataMap = new HashMap<String, String>();
metaDataMap.put(AFTER_STEP.getPropertyName(), "destroy");
metaDataMap.put(AFTER_CHUNK.getPropertyName(), "afterChunk");
factoryBean.setMetaDataMap(metaDataMap);
// Map<String, String> metaDataMap = new HashMap<String, String>();
// metaDataMap.put(AFTER_STEP.getPropertyName(), "destroy");
// metaDataMap.put(AFTER_CHUNK.getPropertyName(), "afterChunk");
// factoryBean.setMetaDataMap(metaDataMap);
String readItem = "item";
Integer writeItem = 2;
List<Integer> 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<String>) listener).beforeRead();
((ItemReadListener<String>) listener).afterRead(readItem);
((ItemReadListener<String>) 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<String, String> metaDataMap = new HashMap<String, String>();
;
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;

View File

@@ -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");
}
}
}
}

View File

@@ -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 {
}
}

View File

@@ -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<String, String> factory = getStepFactory(items);
@@ -227,6 +228,10 @@ public class SimpleStepFactoryBeanTests {
@Override
public void beforeWrite(List<? extends Object> 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();