OPEN - issue BATCH-771: Refactor Listeners for chunk changes

Change step handler to not process items again if already completed
This commit is contained in:
dsyer
2008-08-22 09:07:15 +00:00
parent 6c76c7ae6d
commit c208014073
6 changed files with 183 additions and 90 deletions

View File

@@ -134,4 +134,12 @@ public class StepContribution {
+ ", writeSkips=" + writeSkipCount + "]";
}
/**
* @param contribution
*/
public void increment(StepContribution contribution) {
itemCount += contribution.getItemCount();
readSkipCount += contribution.getReadSkipCount();
}
}

View File

@@ -38,7 +38,7 @@ import org.springframework.core.AttributeAccessor;
* {@link ItemWriter}.
*
* Provides extension points by protected {@link #read(StepContribution)} and
* {@link #write(Object, StepContribution)} methods that can be overriden to
* {@link #write(List, StepContribution)} methods that can be overriden to
* provide more sophisticated behavior (e.g. skipping).
*
* @author Dave Syer
@@ -46,7 +46,9 @@ import org.springframework.core.AttributeAccessor;
*/
public class ItemOrientedStepHandler<T, S> implements StepHandler {
private static final String ITEM_BUFFER_KEY = ItemOrientedStepHandler.class.getName() + ".ITEM_BUFFER_KEY";
private static final String INPUT_BUFFER_KEY = "INPUT_BUFFER_KEY";
private static final String OUTPUT_BUFFER_KEY = "OUTPUT_BUFFER_KEY";
protected final Log logger = LogFactory.getLog(getClass());
@@ -76,7 +78,7 @@ public class ItemOrientedStepHandler<T, S> implements StepHandler {
/**
* Get the next item from {@link #read(StepContribution)} and if not null
* pass the item to {@link #write(Object, StepContribution)}. If the
* pass the item to {@link #write(List, StepContribution)}. If the
* {@link ItemProcessor} returns null, the write is omitted and another item
* taken from the reader.
*
@@ -85,51 +87,53 @@ public class ItemOrientedStepHandler<T, S> implements StepHandler {
*/
public ExitStatus handle(final StepContribution contribution, AttributeAccessor attributes) throws Exception {
final List<ReadWrapper<T>> buffer = getItemBuffer(attributes);
final List<ItemWrapper<T>> inputs = getInputBuffer(attributes);
final List<ItemWrapper<S>> outputs = getOutputBuffer(attributes);
ExitStatus result = ExitStatus.CONTINUABLE;
if (buffer.isEmpty()) {
if (inputs.isEmpty() && outputs.isEmpty()) {
result = repeatOperations.iterate(new RepeatCallback() {
public ExitStatus doInIteration(final RepeatContext context) throws Exception {
ReadWrapper<T> item = read(contribution);
ItemWrapper<T> item = read(contribution);
contribution.incrementReadSkipCount(item.getSkipCount());
if (item.getItem() == null) {
return ExitStatus.FINISHED;
}
buffer.add(item);
inputs.add(item);
return ExitStatus.CONTINUABLE;
}
});
storeInputs(attributes, inputs);
}
List<S> processed = new ArrayList<S>();
for (Iterator<ItemWrapper<T>> iterator = inputs.iterator(); iterator.hasNext();) {
for (Iterator<ReadWrapper<T>> iterator = buffer.iterator(); iterator.hasNext();) {
ReadWrapper<T> item = iterator.next();
ItemWrapper<T> item = iterator.next();
S output = null;
// TODO: processor listener
output = itemProcessor.process(item.getItem());
// TODO: segregate read / write / filter count
// (this is read count)
contribution.incrementItemCount();
// TODO: processor listener
output = itemProcessor.process(item.getItem());
// TODO: increment filter count if this is null
if (output != null) {
processed.add(output);
outputs.add(new ItemWrapper<S>(output));
}
}
storeOutputsAndClearInputs(attributes, outputs, contribution);
// TODO: use ItemWriter interface properly
// TODO: make sure exceptions get handled by the appropriate handler
for (S data : processed) {
write(data, contribution);
}
write(outputs, contribution);
// On successful completion clear the attributes to signal that there is
// no more processing
@@ -140,18 +144,65 @@ public class ItemOrientedStepHandler<T, S> implements StepHandler {
}
/**
* @param attributes
*/
private void clearInputs(AttributeAccessor attributes) {
attributes.removeAttribute(INPUT_BUFFER_KEY);
}
/**
* @param attributes
* @param inputs
*/
private void storeInputs(AttributeAccessor attributes, List<ItemWrapper<T>> inputs) {
store(attributes, INPUT_BUFFER_KEY, inputs);
}
/**
* Savepoint at end of processing and before writing. The processed items
* ready for output are stored so that if writing fails they can be picked
* up again in the next try. The inputs are finished with so we can clear
* their attribute.
*
* @param attributes
* @param outputs
*/
private void storeOutputsAndClearInputs(AttributeAccessor attributes, List<ItemWrapper<S>> outputs,
StepContribution contribution) {
store(attributes, OUTPUT_BUFFER_KEY, outputs);
clearInputs(attributes);
}
/**
* @param attributes
* @param inputBufferKey
* @param outputs
*/
private <W> void store(AttributeAccessor attributes, String key, W value) {
attributes.setAttribute(key, value);
}
private void clearAll(AttributeAccessor attributes) {
for (String key : attributes.attributeNames()) {
attributes.removeAttribute(key);
}
}
private List<ReadWrapper<T>> getItemBuffer(AttributeAccessor attributes) {
if (!attributes.hasAttribute(ITEM_BUFFER_KEY)) {
attributes.setAttribute(ITEM_BUFFER_KEY, new ArrayList<ReadWrapper<T>>());
private List<ItemWrapper<T>> getInputBuffer(AttributeAccessor attributes) {
return getBuffer(attributes, INPUT_BUFFER_KEY);
}
private List<ItemWrapper<S>> getOutputBuffer(AttributeAccessor attributes) {
return getBuffer(attributes, OUTPUT_BUFFER_KEY);
}
private <W> List<W> getBuffer(AttributeAccessor attributes, String key) {
if (!attributes.hasAttribute(key)) {
return new ArrayList<W>();
}
@SuppressWarnings("unchecked")
List<ReadWrapper<T>> resource = (List<ReadWrapper<T>>) attributes.getAttribute(ITEM_BUFFER_KEY);
List<W> resource = (List<W>) attributes.getAttribute(key);
return resource;
}
@@ -159,8 +210,8 @@ public class ItemOrientedStepHandler<T, S> implements StepHandler {
* @param contribution current context
* @return next item for writing
*/
protected ReadWrapper<T> read(StepContribution contribution) throws Exception {
return new ReadWrapper<T>(doRead());
protected ItemWrapper<T> read(StepContribution contribution) throws Exception {
return new ItemWrapper<T>(doRead());
}
/**
@@ -173,27 +224,29 @@ public class ItemOrientedStepHandler<T, S> implements StepHandler {
/**
*
* @param item the item to write
* @param items the item to write
* @param contribution current context
*/
protected void write(S item, StepContribution contribution) throws Exception {
doWrite(item);
protected void write(List<ItemWrapper<S>> items, StepContribution contribution) throws Exception {
for (ItemWrapper<S> item : items) {
doWrite(item);
}
}
/**
* @param item
* @throws Exception
*/
protected final void doWrite(S item) throws Exception {
protected final void doWrite(ItemWrapper<S> item) throws Exception {
// TODO: increment write count
itemWriter.write(Collections.singletonList(item));
itemWriter.write(Collections.singletonList(item.getItem()));
}
/**
* @author Dave Syer
*
*/
static protected class ReadWrapper<T> {
static protected class ItemWrapper<T> {
final private T item;
@@ -202,7 +255,7 @@ public class ItemOrientedStepHandler<T, S> implements StepHandler {
/**
* @param item
*/
public ReadWrapper(T item) {
public ItemWrapper(T item) {
this(item, 0);
}
@@ -210,7 +263,7 @@ public class ItemOrientedStepHandler<T, S> implements StepHandler {
* @param item
* @param skipCount
*/
public ReadWrapper(T item, int skipCount) {
public ItemWrapper(T item, int skipCount) {
this.item = item;
this.skipCount = skipCount;
}
@@ -230,6 +283,16 @@ public class ItemOrientedStepHandler<T, S> implements StepHandler {
return skipCount;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return String.format("[%s,%d]", item, skipCount);
}
}
}

View File

@@ -274,8 +274,8 @@ public class SkipLimitStepFactoryBean<T, S> extends SimpleStepFactoryBean<T, S>
}
});
StatefulRetryStepHandler<T, S> itemHandler = new StatefulRetryStepHandler<T, S>(getItemReader(),
getItemProcessor(), getItemWriter(), getChunkOperations(), retryTemplate, itemKeyGenerator, readSkipPolicy,
writeSkipPolicy);
getItemProcessor(), getItemWriter(), getChunkOperations(), retryTemplate, itemKeyGenerator,
readSkipPolicy, writeSkipPolicy);
itemHandler.setSkipListeners(BatchListenerFactoryHelper.getSkipListeners(getListeners()));
step.setStepHandler(itemHandler);
@@ -327,9 +327,9 @@ public class SkipLimitStepFactoryBean<T, S> extends SimpleStepFactoryBean<T, S>
* @param itemKeyGenerator
*/
public StatefulRetryStepHandler(ItemReader<? extends T> itemReader,
ItemProcessor<? super T, ? extends S> itemProcessor, ItemWriter<? super S> itemWriter, RepeatOperations chunkOperations,
RetryOperations retryTemplate, ItemKeyGenerator itemKeyGenerator, ItemSkipPolicy readSkipPolicy,
ItemSkipPolicy writeSkipPolicy) {
ItemProcessor<? super T, ? extends S> itemProcessor, ItemWriter<? super S> itemWriter,
RepeatOperations chunkOperations, RetryOperations retryTemplate, ItemKeyGenerator itemKeyGenerator,
ItemSkipPolicy readSkipPolicy, ItemSkipPolicy writeSkipPolicy) {
super(itemReader, itemProcessor, itemWriter, chunkOperations);
this.retryOperations = retryTemplate;
this.itemKeyGenerator = itemKeyGenerator;
@@ -368,13 +368,13 @@ public class SkipLimitStepFactoryBean<T, S> extends SimpleStepFactoryBean<T, S>
* count
* @return next item for processing
*/
protected ReadWrapper<T> read(StepContribution contribution) throws Exception {
protected ItemWrapper<T> read(StepContribution contribution) throws Exception {
int skipCount = 0;
while (true) {
try {
return new ReadWrapper<T>(doRead(), skipCount);
return new ItemWrapper<T>(doRead(), skipCount);
}
catch (Exception e) {
try {
@@ -410,37 +410,48 @@ public class SkipLimitStepFactoryBean<T, S> extends SimpleStepFactoryBean<T, S>
/**
* Execute the business logic, delegating to the writer.<br/>
*
* Process the item with the {@link ItemWriter} in a stateful retry. Any
* Process the items with the {@link ItemWriter} in a stateful retry. Any
* {@link SkipListener} provided is called when retry attempts are
* exhausted. The listener callback (on write failure) will happen in
* the next transaction automatically.<br/>
*/
@Override
protected void write(final S item, final StepContribution contribution) throws Exception {
RecoveryRetryCallback retryCallback = new RecoveryRetryCallback(item, new RetryCallback() {
public Object doWithRetry(RetryContext context) throws Throwable {
doWrite(item);
return null;
}
}, itemKeyGenerator != null ? itemKeyGenerator.getKey(item) : item);
retryCallback.setRecoveryCallback(new RecoveryCallback() {
public Object recover(RetryContext context) {
Throwable t = context.getLastThrowable();
if (writeSkipPolicy.shouldSkip(t, contribution.getStepSkipCount())) {
contribution.incrementWriteSkipCount();
try {
listener.onSkipInWrite(item, t);
}
catch (RuntimeException ex) {
throw new SkipListenerFailedException("Fatal exception in SkipListener.", ex, t);
}
} else {
throw new RetryException("Non-skippable exception in recoverer", t);
protected void write(final List<ItemWrapper<S>> items, final StepContribution contribution) throws Exception {
for (final ItemWrapper<S> item : new ArrayList<ItemWrapper<S>>(items)) {
RecoveryRetryCallback retryCallback = new RecoveryRetryCallback(item, new RetryCallback() {
public Object doWithRetry(RetryContext context) throws Throwable {
doWrite(item);
return null;
}
return null;
}
});
retryOperations.execute(retryCallback);
}, itemKeyGenerator != null ? itemKeyGenerator.getKey(item.getItem()) : item);
retryCallback.setRecoveryCallback(new RecoveryCallback() {
public Object recover(RetryContext context) {
Throwable t = context.getLastThrowable();
if (writeSkipPolicy.shouldSkip(t, contribution.getStepSkipCount())) {
contribution.incrementWriteSkipCount();
items.remove(item);
try {
listener.onSkipInWrite(item.getItem(), t);
}
catch (RuntimeException ex) {
throw new SkipListenerFailedException("Fatal exception in SkipListener.", ex, t);
}
}
else {
throw new RetryException("Non-skippable exception in recoverer", t);
}
return null;
}
});
retryOperations.execute(retryCallback);
}
}
}

View File

@@ -233,8 +233,7 @@ public class StepHandlerStep extends AbstractStep {
AttributeAccessor attributes = attributeQueue.poll();
if (attributes == null) {
attributes = new AttributeAccessorSupport() {
};
attributes = new BasicAttributeAccessor();
}
try {
@@ -258,6 +257,11 @@ public class StepHandlerStep extends AbstractStep {
if (attributes.attributeNames().length > 0) {
attributeQueue.add(attributes);
}
// Apply the contribution to the step
// even if unsuccessful
stepExecution.apply(contribution);
}
contribution.incrementCommitCount();
@@ -274,10 +278,6 @@ public class StepHandlerStep extends AbstractStep {
Thread.currentThread().interrupt();
}
// Apply the contribution to the step
// only if chunk was successful
stepExecution.apply(contribution);
try {
stream.update(stepExecution.getExecutionContext());
}
@@ -322,11 +322,11 @@ public class StepHandlerStep extends AbstractStep {
}
catch (Error e) {
processRollback(stepExecution, contribution, fatalException, transaction);
processRollback(stepExecution, fatalException, transaction);
throw e;
}
catch (Exception e) {
processRollback(stepExecution, contribution, fatalException, transaction);
processRollback(stepExecution, fatalException, transaction);
throw e;
}
finally {
@@ -351,15 +351,12 @@ public class StepHandlerStep extends AbstractStep {
/**
* @param stepExecution
* @param contribution
* @param fatalException
* @param transaction
*/
private void processRollback(final StepExecution stepExecution, final StepContribution contribution,
final ExceptionHolder fatalException, TransactionStatus transaction) {
private void processRollback(final StepExecution stepExecution, final ExceptionHolder fatalException,
TransactionStatus transaction) {
stepExecution.incrementReadSkipCountBy(contribution.getReadSkipCount());
stepExecution.incrementWriteSkipCountBy(contribution.getWriteSkipCount());
/*
* Any exception thrown within the transaction should automatically
* cause the transaction to rollback.
@@ -387,6 +384,13 @@ public class StepHandlerStep extends AbstractStep {
}
}
/**
* @author Dave Syer
*
*/
private static final class BasicAttributeAccessor extends AttributeAccessorSupport {
}
private static class ExceptionHolder {
private Exception exception;

View File

@@ -343,14 +343,13 @@ public class SkipLimitStepFactoryBeanTests {
factory.setSkipLimit(4);
factory.setItemReader(reader);
factory.setItemWriter(writer);
factory.setCommitInterval(3); // includes all expected skips
Step step = (Step) factory.getObject();
StepExecution stepExecution = jobExecution.createStepExecution(step);
step.execute(stepExecution);
System.err.println(writer.written);
System.err.println(reader.processed);
assertEquals(4, stepExecution.getSkipCount());
assertEquals(2, stepExecution.getReadSkipCount());
assertEquals(2, stepExecution.getWriteSkipCount());

View File

@@ -43,6 +43,7 @@ import org.springframework.batch.core.repository.dao.MapStepExecutionDao;
import org.springframework.batch.core.repository.support.SimpleJobRepository;
import org.springframework.batch.core.step.AbstractStep;
import org.springframework.batch.core.step.skip.SkipLimitExceededException;
import org.springframework.batch.item.ItemKeyGenerator;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.support.ListItemReader;
@@ -283,17 +284,17 @@ public class StatefulRetryStepFactoryBeanTests {
assertEquals(4, processed.size());
// []
assertEquals(0, recovered.size());
assertEquals(0, stepExecution.getItemCount());
assertEquals(1, stepExecution.getItemCount());
}
@Test
public void testNonSkippableException() throws Exception {
// Very specific skippable exception
factory.setSkippableExceptionClasses(new Class[] { UnsupportedOperationException.class });
// ...which is not retryable...
factory.setRetryableExceptionClasses(new Class<?>[0]);
factory.setSkipLimit(1);
List<String> items = Arrays.asList(new String[] { "b" });
ItemReader<String> provider = new ListItemReader<String>(items) {
@@ -323,7 +324,7 @@ public class StatefulRetryStepFactoryBeanTests {
catch (RuntimeException e) {
// expected
String message = e.getMessage();
assertTrue("Wrong message: "+message, message.contains("Write error - planned but not skippable."));
assertTrue("Wrong message: " + message, message.contains("Write error - planned but not skippable."));
}
assertEquals(0, stepExecution.getSkipCount());
@@ -333,7 +334,7 @@ public class StatefulRetryStepFactoryBeanTests {
assertEquals(1, processed.size());
// []
assertEquals(0, recovered.size());
assertEquals(0, stepExecution.getItemCount());
assertEquals(1, stepExecution.getItemCount());
}
@Test
@@ -376,7 +377,7 @@ public class StatefulRetryStepFactoryBeanTests {
assertEquals(4, processed.size());
// []
assertEquals(0, recovered.size());
assertEquals(0, stepExecution.getItemCount());
assertEquals(1, stepExecution.getItemCount());
}
@Test
@@ -390,7 +391,7 @@ public class StatefulRetryStepFactoryBeanTests {
factory.setCacheCapacity(2);
ItemReader<String> provider = new ItemReader<String>() {
public String read() {
String item = ""+count;
String item = "" + count;
provided.add(item);
count++;
if (count >= 10) {
@@ -409,6 +410,12 @@ public class StatefulRetryStepFactoryBeanTests {
};
factory.setItemReader(provider);
factory.setItemWriter(itemWriter);
factory.setItemKeyGenerator(new ItemKeyGenerator() {
public Object getKey(Object item) {
// return random object so the cache fills up
return new Object();
}
});
AbstractStep step = (AbstractStep) factory.getObject();
StepExecution stepExecution = new StepExecution(step.getName(), jobExecution);
@@ -420,13 +427,14 @@ public class StatefulRetryStepFactoryBeanTests {
// expected
}
assertEquals(1, stepExecution.getSkipCount());
// We added a bogus key generator so no items are actually skipped
// because they aren't recognised as eligible
assertEquals(0, stepExecution.getSkipCount());
// only one item processed but three (the commit interval) were provided
// [0, 1, 2]
assertEquals(3, provided.size());
// TODO: this is a bug: 0 was skipped but it came back in the buffer for the second try
// [0, 0, 1, 0, 0]
assertEquals(5, processed.size());
// [0, 0, 0]
assertEquals(3, processed.size());
// []
assertEquals(0, recovered.size());
}