/* * Copyright 2008-2014 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.step.item; import org.junit.Before; import org.junit.Test; import org.springframework.batch.core.ExitStatus; import org.springframework.batch.core.JobExecution; import org.springframework.batch.core.JobInstance; import org.springframework.batch.core.JobInterruptedException; import org.springframework.batch.core.JobParameters; import org.springframework.batch.core.Step; import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.StepExecutionListener; import org.springframework.batch.core.listener.StepExecutionListenerSupport; import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException; import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException; import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.repository.JobRestartException; import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.core.step.tasklet.Tasklet; import org.springframework.batch.core.step.tasklet.TaskletStep; import org.springframework.batch.item.ExecutionContext; import org.springframework.batch.item.ItemStream; import org.springframework.batch.item.ItemStreamException; import org.springframework.batch.item.ItemStreamSupport; import org.springframework.batch.repeat.RepeatStatus; import org.springframework.batch.support.transaction.ResourcelessTransactionManager; import org.springframework.transaction.TransactionException; import org.springframework.transaction.UnexpectedRollbackException; import org.springframework.transaction.support.DefaultTransactionStatus; import org.springframework.transaction.support.TransactionSynchronizationManager; import java.util.Collection; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.springframework.batch.core.BatchStatus.COMPLETED; import static org.springframework.batch.core.BatchStatus.FAILED; import static org.springframework.batch.core.BatchStatus.STOPPED; import static org.springframework.batch.core.BatchStatus.UNKNOWN; /** * Tests for the behavior of TaskletStep in a failure scenario. * * @author Lucas Ward * @author Dave Syer * @author David Turanski * */ public class TaskletStepExceptionTests { TaskletStep taskletStep; StepExecution stepExecution; UpdateCountingJobRepository jobRepository; static RuntimeException taskletException = new RuntimeException("Static planned test exception."); static JobInterruptedException interruptedException = new JobInterruptedException(""); @Before public void init() { taskletStep = new TaskletStep(); taskletStep.setTasklet(new ExceptionTasklet()); jobRepository = new UpdateCountingJobRepository(); taskletStep.setJobRepository(jobRepository); taskletStep.setTransactionManager(new ResourcelessTransactionManager()); JobInstance jobInstance = new JobInstance(1L, "testJob"); JobExecution jobExecution = new JobExecution(jobInstance, new JobParameters()); stepExecution = new StepExecution("testStep", jobExecution); } @Test public void testApplicationException() throws Exception { taskletStep.execute(stepExecution); assertEquals(FAILED, stepExecution.getStatus()); assertEquals(FAILED.toString(), stepExecution.getExitStatus().getExitCode()); } @Test public void testInterrupted() throws Exception { taskletStep.setStepExecutionListeners(new StepExecutionListener[] { new InterruptionListener() }); taskletStep.execute(stepExecution); assertEquals(STOPPED, stepExecution.getStatus()); assertEquals(STOPPED.toString(), stepExecution.getExitStatus().getExitCode()); } @Test public void testInterruptedWithCustomStatus() throws Exception { taskletStep.setTasklet(new Tasklet() { @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { contribution.setExitStatus(new ExitStatus("FUNNY")); throw new JobInterruptedException("Planned"); } }); taskletStep.execute(stepExecution); assertEquals(STOPPED, stepExecution.getStatus()); assertEquals("FUNNY", stepExecution.getExitStatus().getExitCode()); } @Test public void testOpenFailure() throws Exception { final RuntimeException exception = new RuntimeException(); taskletStep.setStreams(new ItemStream[] { new ItemStreamSupport() { @Override public void open(ExecutionContext executionContext) throws ItemStreamException { throw exception; } } }); taskletStep.execute(stepExecution); assertEquals(FAILED, stepExecution.getStatus()); assertTrue(stepExecution.getFailureExceptions().contains(exception)); assertEquals(2, jobRepository.getUpdateCount()); } @Test public void testBeforeStepFailure() throws Exception { final RuntimeException exception = new RuntimeException(); taskletStep.setStepExecutionListeners(new StepExecutionListenerSupport[] { new StepExecutionListenerSupport() { @Override public void beforeStep(StepExecution stepExecution) { throw exception; } } }); taskletStep.execute(stepExecution); assertEquals(FAILED, stepExecution.getStatus()); assertTrue(stepExecution.getFailureExceptions().contains(exception)); assertEquals(2, jobRepository.getUpdateCount()); } @Test public void testAfterStepFailureWhenTaskletSucceeds() throws Exception { final RuntimeException exception = new RuntimeException(); taskletStep.setStepExecutionListeners(new StepExecutionListenerSupport[] { new StepExecutionListenerSupport() { @Override public ExitStatus afterStep(StepExecution stepExecution) { throw exception; } } }); taskletStep.setTasklet(new Tasklet() { @Override public RepeatStatus execute(StepContribution contribution, ChunkContext attributes) throws Exception { return RepeatStatus.FINISHED; } }); taskletStep.execute(stepExecution); assertEquals(COMPLETED, stepExecution.getStatus()); assertFalse(stepExecution.getFailureExceptions().contains(exception)); assertEquals(3, jobRepository.getUpdateCount()); } @Test /* * Exception in afterStep is ignored (only logged). */ public void testAfterStepFailureWhenTaskletFails() throws Exception { final RuntimeException exception = new RuntimeException(); taskletStep.setStepExecutionListeners(new StepExecutionListenerSupport[] { new StepExecutionListenerSupport() { @Override public ExitStatus afterStep(StepExecution stepExecution) { throw exception; } } }); taskletStep.execute(stepExecution); assertEquals(FAILED, stepExecution.getStatus()); assertTrue(stepExecution.getFailureExceptions().contains(taskletException)); assertFalse(stepExecution.getFailureExceptions().contains(exception)); assertEquals(2, jobRepository.getUpdateCount()); } @Test public void testCloseError() throws Exception { final RuntimeException exception = new RuntimeException(); taskletStep.setStreams(new ItemStream[] { new ItemStreamSupport() { @Override public void close() throws ItemStreamException { super.close(); throw exception; } } }); taskletStep.execute(stepExecution); assertEquals(FAILED, stepExecution.getStatus()); assertTrue(stepExecution.getFailureExceptions().contains(taskletException)); assertTrue(stepExecution.getFailureExceptions().contains(exception)); assertEquals(2, jobRepository.getUpdateCount()); } @SuppressWarnings("serial") @Test public void testCommitError() throws Exception { taskletStep.setTransactionManager(new ResourcelessTransactionManager() { @Override protected void doCommit(DefaultTransactionStatus status) throws TransactionException { throw new RuntimeException("bar"); } @Override protected void doRollback(DefaultTransactionStatus status) throws TransactionException { throw new RuntimeException("foo"); } }); taskletStep.setTasklet(new Tasklet() { @Override public RepeatStatus execute(StepContribution contribution, ChunkContext attributes) throws Exception { attributes.getStepContext().getStepExecution().getExecutionContext().putString("foo", "bar"); return RepeatStatus.FINISHED; } }); taskletStep.execute(stepExecution); assertEquals(UNKNOWN, stepExecution.getStatus()); Throwable e = stepExecution.getFailureExceptions().get(0); assertEquals("foo", e.getMessage()); assertEquals(0, stepExecution.getCommitCount()); assertEquals(1, stepExecution.getRollbackCount()); // Failed transaction // counts as // rollback assertEquals(2, stepExecution.getExecutionContext().size()); assertTrue(stepExecution.getExecutionContext().containsKey(Step.STEP_TYPE_KEY)); assertTrue(stepExecution.getExecutionContext().containsKey(TaskletStep.TASKLET_TYPE_KEY)); } @SuppressWarnings("serial") @Test public void testUnexpectedRollback() throws Exception { taskletStep.setTransactionManager(new ResourcelessTransactionManager() { @Override protected void doCommit(DefaultTransactionStatus status) throws TransactionException { super.doRollback(status); throw new UnexpectedRollbackException("bar"); } }); taskletStep.setTasklet(new Tasklet() { @Override public RepeatStatus execute(StepContribution contribution, ChunkContext attributes) throws Exception { attributes.getStepContext().getStepExecution().getExecutionContext().putString("foo", "bar"); return RepeatStatus.FINISHED; } }); taskletStep.execute(stepExecution); assertEquals(FAILED, stepExecution.getStatus()); Throwable e = stepExecution.getFailureExceptions().get(0); assertEquals("bar", e.getMessage()); assertEquals(0, stepExecution.getCommitCount()); assertEquals(1, stepExecution.getRollbackCount()); // Failed transaction // counts as // rollback assertEquals(2, stepExecution.getExecutionContext().size()); assertTrue(stepExecution.getExecutionContext().containsKey(Step.STEP_TYPE_KEY)); assertTrue(stepExecution.getExecutionContext().containsKey(TaskletStep.TASKLET_TYPE_KEY)); } @Test public void testRepositoryErrorOnExecutionContext() throws Exception { taskletStep.setTasklet(new Tasklet() { @Override public RepeatStatus execute(StepContribution contribution, ChunkContext attributes) throws Exception { return RepeatStatus.FINISHED; } }); jobRepository.setFailOnUpdateExecutionContext(true); taskletStep.execute(stepExecution); assertEquals(UNKNOWN, stepExecution.getStatus()); Throwable e = stepExecution.getFailureExceptions().get(0); assertEquals("Expected exception in step execution context persistence", e.getMessage()); } @Test public void testRepositoryErrorOnExecutionContextInTransaction() throws Exception { taskletStep.setTasklet(new Tasklet() { @Override public RepeatStatus execute(StepContribution contribution, ChunkContext attributes) throws Exception { return RepeatStatus.FINISHED; } }); jobRepository.setFailOnUpdateExecutionContext(true); jobRepository.setFailInTransaction(true); taskletStep.execute(stepExecution); assertEquals(FAILED, stepExecution.getStatus()); Throwable e = stepExecution.getFailureExceptions().get(0); assertEquals("JobRepository failure forcing rollback", e.getMessage()); } @Test public void testRepositoryErrorOnExecutionContextInTransactionRollbackFailed() throws Exception { taskletStep.setTasklet(new Tasklet() { @Override public RepeatStatus execute(StepContribution contribution, ChunkContext attributes) throws Exception { return RepeatStatus.FINISHED; } }); taskletStep.setTransactionManager(new FailingRollbackTransactionManager()); jobRepository.setFailOnUpdateExecutionContext(true); jobRepository.setFailInTransaction(true); taskletStep.execute(stepExecution); assertEquals(UNKNOWN, stepExecution.getStatus()); Throwable e = stepExecution.getFailureExceptions().get(0); assertEquals("Expected exception in rollback", e.getMessage()); } @Test public void testRepositoryErrorOnUpdateStepExecution() throws Exception { taskletStep.setTasklet(new Tasklet() { @Override public RepeatStatus execute(StepContribution contribution, ChunkContext attributes) throws Exception { return RepeatStatus.FINISHED; } }); jobRepository.setFailOnUpdateStepExecution(2); taskletStep.execute(stepExecution); assertEquals(UNKNOWN, stepExecution.getStatus()); Throwable e = stepExecution.getFailureExceptions().get(0); assertEquals("Expected exception in step execution persistence", e.getMessage()); } @Test public void testRepositoryErrorOnUpdateStepExecutionInTransaction() throws Exception { taskletStep.setTasklet(new Tasklet() { @Override public RepeatStatus execute(StepContribution contribution, ChunkContext attributes) throws Exception { return RepeatStatus.FINISHED; } }); jobRepository.setFailOnUpdateStepExecution(1); jobRepository.setFailInTransaction(true); taskletStep.execute(stepExecution); assertEquals(FAILED, stepExecution.getStatus()); Throwable e = stepExecution.getFailureExceptions().get(0); assertEquals("JobRepository failure forcing rollback", e.getMessage()); } @Test public void testRepositoryErrorOnUpdateStepExecutionInTransactionRollbackFailed() throws Exception { taskletStep.setTasklet(new Tasklet() { @Override public RepeatStatus execute(StepContribution contribution, ChunkContext attributes) throws Exception { return RepeatStatus.FINISHED; } }); taskletStep.setTransactionManager(new FailingRollbackTransactionManager()); jobRepository.setFailOnUpdateStepExecution(1); jobRepository.setFailInTransaction(true); taskletStep.execute(stepExecution); assertEquals(UNKNOWN, stepExecution.getStatus()); Throwable e = stepExecution.getFailureExceptions().get(0); assertEquals("Expected exception in rollback", e.getMessage()); } @Test public void testRepositoryErrorOnFailure() throws Exception { taskletStep.setTasklet(new Tasklet() { @Override public RepeatStatus execute(StepContribution contribution, ChunkContext attributes) throws Exception { throw new RuntimeException("Tasklet exception"); } }); jobRepository.setFailOnUpdateExecutionContext(true); taskletStep.execute(stepExecution); assertEquals(UNKNOWN, stepExecution.getStatus()); Throwable e = stepExecution.getFailureExceptions().get(0); assertEquals("Expected exception in step execution context persistence", e.getMessage()); } @Test public void testUpdateError() throws Exception { final RuntimeException exception = new RuntimeException(); taskletStep.setJobRepository(new UpdateCountingJobRepository() { boolean firstCall = true; @Override public void update(StepExecution arg0) { if (firstCall) { firstCall = false; return; } throw exception; } }); taskletStep.execute(stepExecution); assertEquals(UNKNOWN, stepExecution.getStatus()); assertTrue(stepExecution.getFailureExceptions().contains(exception)); assertTrue(stepExecution.getFailureExceptions().contains(taskletException)); } private static class ExceptionTasklet implements Tasklet { @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { throw taskletException; } } private static class InterruptionListener extends StepExecutionListenerSupport { @Override public void beforeStep(StepExecution stepExecution) { stepExecution.setTerminateOnly(); } } private static class UpdateCountingJobRepository implements JobRepository { private int updateCount = 0; private boolean failOnUpdateContext = false; private int failOnUpdateExecution = -1; private boolean failInTransaction = false; public void setFailOnUpdateExecutionContext(boolean failOnUpdate) { this.failOnUpdateContext = failOnUpdate; } public void setFailOnUpdateStepExecution(int failOnUpdate) { this.failOnUpdateExecution = failOnUpdate; } public void setFailInTransaction(boolean failInTransaction) { this.failInTransaction = failInTransaction; } @Override public void add(StepExecution stepExecution) { } @Override public JobExecution createJobExecution(String jobName, JobParameters jobParameters) throws JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException { return null; } @Override public StepExecution getLastStepExecution(JobInstance jobInstance, String stepName) { return null; } @Override public int getStepExecutionCount(JobInstance jobInstance, String stepName) { return 0; } @Override public boolean isJobInstanceExists(String jobName, JobParameters jobParameters) { return false; } @Override public void update(JobExecution jobExecution) { } @Override public void update(StepExecution stepExecution) { if (updateCount++ == failOnUpdateExecution) { if (!failInTransaction || (failInTransaction && TransactionSynchronizationManager.isActualTransactionActive())) { throw new RuntimeException("Expected exception in step execution persistence"); } } } @Override public void updateExecutionContext(StepExecution stepExecution) { if (failOnUpdateContext) { if (!failInTransaction || (failInTransaction && TransactionSynchronizationManager.isActualTransactionActive())) { throw new RuntimeException("Expected exception in step execution context persistence"); } } } public int getUpdateCount() { return updateCount; } @Override public JobExecution getLastJobExecution(String jobName, JobParameters jobParameters) { return null; } @Override public void updateExecutionContext(JobExecution jobExecution) { } @Override public void addAll(Collection<StepExecution> stepExecutions) { } @Override public JobInstance createJobInstance(String jobName, JobParameters jobParameters) { return null; } @Override public JobExecution createJobExecution(JobInstance jobInstance, JobParameters jobParameters, String jobConfigurationLocation) { return null; } } @SuppressWarnings("serial") private static class FailingRollbackTransactionManager extends ResourcelessTransactionManager { @Override protected void doRollback(DefaultTransactionStatus status) throws TransactionException { super.doRollback(status); throw new RuntimeException("Expected exception in rollback"); } } }