Refactored the JsrJobOperator to start and restart jobs asynchronously

by default.
* Refactored the JsrJobOperator to use SimpleAsyncTaskExecutor by
 default
* Introduced a JobContextFactory to provide a JobContext per thread.
* Updated stop logic to correctly handle stopping jobs running on
 another thread.
This commit is contained in:
Michael Minella
2013-12-02 09:54:13 -06:00
parent 8bee7ac310
commit 78cf001e9e
13 changed files with 612 additions and 152 deletions

View File

@@ -0,0 +1,124 @@
/*
* Copyright 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.jsr;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import javax.batch.runtime.context.JobContext;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.jsr.configuration.support.BatchPropertyContext;
import org.springframework.batch.core.scope.context.StepSynchronizationManager;
import org.springframework.beans.factory.FactoryBeanNotInitializedException;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
public class JobContextFactoryBeanTests {
private JobContextFactoryBean factoryBean;
private BatchPropertyContext propertyContext;
@Before
public void setUp() throws Exception {
propertyContext = new BatchPropertyContext();
factoryBean = new JobContextFactoryBean();
}
@After
public void tearDown() throws Exception {
factoryBean.close();
}
@Test
public void testIntialCreationSingleThread() throws Exception {
factoryBean.setJobExecution(new JobExecution(5l));
factoryBean.setBatchPropertyContext(propertyContext);
assertTrue(factoryBean.getObjectType().isAssignableFrom(JobContext.class));
assertFalse(factoryBean.isSingleton());
JobContext jobContext1 = factoryBean.getObject();
JobContext jobContext2 = factoryBean.getObject();
assertEquals(5l, jobContext1.getExecutionId());
assertEquals(5l, jobContext2.getExecutionId());
assertTrue(jobContext1 == jobContext2);
}
@Test
public void testInitialCreationSingleThreadUsingStepScope() throws Exception {
factoryBean.setBatchPropertyContext(propertyContext);
StepSynchronizationManager.register(new StepExecution("step1", new JobExecution(5l)));
JobContext jobContext = factoryBean.getObject();
assertEquals(5l, jobContext.getExecutionId());
StepSynchronizationManager.close();
}
@Test(expected=FactoryBeanNotInitializedException.class)
public void testNoJobExecutionProvided() throws Exception {
factoryBean.getObject();
}
@Test
public void testOneJobContextPerThread() throws Exception {
List<Future<JobContext>> jobContexts = new ArrayList<Future<JobContext>>();
AsyncTaskExecutor executor = new SimpleAsyncTaskExecutor();
for(int i = 0; i < 4; i++) {
final long count = i;
jobContexts.add(executor.submit(new Callable<JobContext>() {
@Override
public JobContext call() throws Exception {
try {
StepSynchronizationManager.register(new StepExecution("step" + count, new JobExecution(count)));
JobContext context = factoryBean.getObject();
Thread.sleep(1000l);
return context;
} catch (Throwable ignore) {
return null;
}finally {
StepSynchronizationManager.release();
}
}
}));
}
Set<JobContext> contexts = new HashSet<JobContext>();
for (Future<JobContext> future : jobContexts) {
contexts.add(future.get());
}
assertEquals(4, contexts.size());
}
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright 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.jsr;
import java.util.Date;
import java.util.Properties;
import java.util.concurrent.TimeoutException;
import javax.batch.operations.JobOperator;
import javax.batch.runtime.BatchRuntime;
import javax.batch.runtime.BatchStatus;
import javax.batch.runtime.JobExecution;
public class JsrTestUtils {
private static JobOperator operator;
static {
operator = BatchRuntime.getJobOperator();
}
private JsrTestUtils() {}
/**
* Executes a job and waits for it's status to be any of {@link BatchStatus#STOPPED},
* {@link BatchStatus#COMPLETED}, or {@link BatchStatus#FAILED}. If the job does not
* reach one of those statuses within the given timeout, a {@link TimeoutException} is
* thrown.
*
* @param jobName
* @param properties
* @param timeout
* @return the {@link JobExecution} for the final state of the job
* @throws TimeoutException if the timeout occurs
*/
public static JobExecution runJob(String jobName, Properties properties, long timeout) throws TimeoutException{
long executionId = operator.start(jobName, properties);
JobExecution execution = operator.getJobExecution(executionId);
Date curDate = new Date();
BatchStatus curBatchStatus = execution.getBatchStatus();
while(true) {
if(curBatchStatus == BatchStatus.STOPPED || curBatchStatus == BatchStatus.COMPLETED || curBatchStatus == BatchStatus.FAILED) {
break;
}
if(new Date().getTime() - curDate.getTime() > timeout) {
throw new TimeoutException("Job processing did not complete in time");
}
execution = operator.getJobExecution(executionId);
curBatchStatus = execution.getBatchStatus();
}
return execution;
}
/**
* Restarts a job and waits for it's status to be any of {@link BatchStatus#STOPPED},
* {@link BatchStatus#COMPLETED}, or {@link BatchStatus#FAILED}. If the job does not
* reach one of those statuses within the given timeout, a {@link TimeoutException} is
* thrown.
*
* @param executionId
* @param properties
* @param timeout
* @return the {@link JobExecution} for the final state of the job
* @throws TimeoutException if the timeout occurs
*/
public static JobExecution restartJob(long executionId, Properties properties, long timeout) throws TimeoutException {
long restartId = operator.restart(executionId, properties);
JobExecution execution = operator.getJobExecution(restartId);
Date curDate = new Date();
BatchStatus curBatchStatus = execution.getBatchStatus();
while(true) {
if(curBatchStatus == BatchStatus.STOPPED || curBatchStatus == BatchStatus.COMPLETED || curBatchStatus == BatchStatus.FAILED) {
break;
}
if(new Date().getTime() - curDate.getTime() > timeout) {
throw new TimeoutException("Job processing did not complete in time");
}
execution = operator.getJobExecution(restartId);
curBatchStatus = execution.getBatchStatus();
}
return execution;
}
}

View File

@@ -17,6 +17,7 @@ package org.springframework.batch.core.jsr.configuration.xml;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.springframework.batch.core.jsr.JsrTestUtils.runJob;
import java.io.Serializable;
import java.util.ArrayList;
@@ -35,28 +36,19 @@ import javax.batch.api.chunk.AbstractItemReader;
import javax.batch.api.chunk.AbstractItemWriter;
import javax.batch.api.partition.PartitionPlan;
import javax.batch.api.partition.PartitionPlanImpl;
import javax.batch.operations.JobOperator;
import javax.batch.runtime.BatchRuntime;
import javax.batch.runtime.BatchStatus;
import javax.batch.runtime.JobExecution;
import javax.batch.runtime.context.JobContext;
import javax.inject.Inject;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.util.Assert;
public class PartitionParserTests {
private static JobOperator operator;
private Pattern caPattern = Pattern.compile("ca");
private Pattern asPattern = Pattern.compile("AS");
@BeforeClass
public static void beforeClass() {
operator = BatchRuntime.getJobOperator();
}
private static final long TIMEOUT = 10000l;
@Before
public void before() {
@@ -67,17 +59,17 @@ public class PartitionParserTests {
}
@Test
public void testBatchletNoProperties() {
JobExecution execution = operator.getJobExecution(operator.start("partitionParserTestsBatchlet", new Properties()));
public void testBatchletNoProperties() throws Exception {
BatchStatus curBatchStatus = runJob("partitionParserTestsBatchlet", new Properties(), TIMEOUT).getBatchStatus();
assertEquals(BatchStatus.COMPLETED, execution.getBatchStatus());
assertEquals(BatchStatus.COMPLETED, curBatchStatus);
assertEquals(10, MyBatchlet.processed);
assertEquals(10, MyBatchlet.threadNames.size());
}
@Test
public void testChunkNoProperties() {
JobExecution execution = operator.getJobExecution(operator.start("partitionParserTestsChunk", new Properties()));
public void testChunkNoProperties() throws Exception {
JobExecution execution = runJob("partitionParserTestsChunk", new Properties(), TIMEOUT);
assertEquals(BatchStatus.COMPLETED, execution.getBatchStatus());
assertEquals(30, ItemReader.processedItems.size());
@@ -87,8 +79,8 @@ public class PartitionParserTests {
}
@Test
public void testFullPartitionConfiguration() {
JobExecution execution = operator.getJobExecution(operator.start("fullPartitionParserTests", new Properties()));
public void testFullPartitionConfiguration() throws Exception {
JobExecution execution = runJob("fullPartitionParserTests", new Properties(), TIMEOUT);
assertEquals(BatchStatus.COMPLETED, execution.getBatchStatus());
assertTrue(execution.getExitStatus().startsWith("BPS_"));
@@ -100,8 +92,8 @@ public class PartitionParserTests {
}
@Test
public void testFullPartitionConfigurationWithProperties() {
JobExecution execution = operator.getJobExecution(operator.start("fullPartitionParserWithPropertiesTests", new Properties()));
public void testFullPartitionConfigurationWithProperties() throws Exception {
JobExecution execution = runJob("fullPartitionParserWithPropertiesTests", new Properties(), TIMEOUT);
assertEquals(BatchStatus.COMPLETED, execution.getBatchStatus());
assertTrue(execution.getExitStatus().startsWith("BPS_"));
@@ -119,8 +111,8 @@ public class PartitionParserTests {
}
@Test
public void testFullPartitionConfigurationWithMapperSuppliedProperties() {
JobExecution execution = operator.getJobExecution(operator.start("fullPartitionParserWithMapperPropertiesTests", new Properties()));
public void testFullPartitionConfigurationWithMapperSuppliedProperties() throws Exception {
JobExecution execution = runJob("fullPartitionParserWithMapperPropertiesTests", new Properties(), TIMEOUT);
assertEquals(BatchStatus.COMPLETED, execution.getBatchStatus());
assertTrue(execution.getExitStatus().startsWith("BPS_"));
@@ -145,8 +137,8 @@ public class PartitionParserTests {
}
@Test
public void testFullPartitionConfigurationWithHardcodedProperties() {
JobExecution execution = operator.getJobExecution(operator.start("fullPartitionParserWithHardcodedPropertiesTests", new Properties()));
public void testFullPartitionConfigurationWithHardcodedProperties() throws Exception {
JobExecution execution = runJob("fullPartitionParserWithHardcodedPropertiesTests", new Properties(), TIMEOUT);
assertEquals(BatchStatus.COMPLETED, execution.getBatchStatus());
assertTrue(execution.getExitStatus().startsWith("BPS_"));

View File

@@ -16,29 +16,19 @@
package org.springframework.batch.core.jsr.configuration.xml;
import static org.junit.Assert.assertEquals;
import static org.springframework.batch.core.jsr.JsrTestUtils.runJob;
import java.util.Properties;
import javax.batch.operations.JobOperator;
import javax.batch.runtime.BatchRuntime;
import javax.batch.runtime.BatchStatus;
import javax.batch.runtime.JobExecution;
import org.junit.Before;
import org.junit.Test;
public class ThreadLocalClassloaderBeanPostProcessorTests {
private JobOperator jobOperator;
@Before
public void setUp() throws Exception {
jobOperator = BatchRuntime.getJobOperator();
}
@Test
public void test() throws Exception {
long executionId = jobOperator.start("threadLocalClassloaderBeanPostProcessorTestsJob", new Properties());
JobExecution execution = runJob("threadLocalClassloaderBeanPostProcessorTestsJob", null, 10000);
assertEquals(BatchStatus.COMPLETED, jobOperator.getJobExecution(executionId).getBatchStatus());
assertEquals(BatchStatus.COMPLETED, execution.getBatchStatus());
}
}

View File

@@ -20,9 +20,10 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.batch.core.jsr.JsrTestUtils.restartJob;
import static org.springframework.batch.core.jsr.JsrTestUtils.runJob;
import java.util.ArrayList;
import java.util.Date;
@@ -31,6 +32,7 @@ import java.util.List;
import java.util.Properties;
import java.util.Set;
import javax.batch.api.Batchlet;
import javax.batch.operations.JobExecutionIsRunningException;
import javax.batch.operations.JobOperator;
import javax.batch.operations.JobRestartException;
@@ -55,29 +57,27 @@ import org.springframework.batch.core.converter.JobParametersConverterSupport;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.explore.support.SimpleJobExplorer;
import org.springframework.batch.core.jsr.JsrJobParametersConverter;
import org.springframework.batch.core.launch.support.SimpleJobOperator;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.JobRepositorySupport;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.SyncTaskExecutor;
public class JsrJobOperatorTests {
private JobOperator jsrJobOperator;
@Mock
private org.springframework.batch.core.launch.JobOperator jobOperator;
@Mock
private JobExplorer jobExplorer;
@Mock
private JobRepository jobRepository;
private JobParametersConverter parameterConverter;
private static final long TIMEOUT = 10000l;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
parameterConverter = new JobParametersConverterSupport();
jsrJobOperator = new JsrJobOperator(jobExplorer, jobRepository, jobOperator, parameterConverter);
jsrJobOperator = new JsrJobOperator(jobExplorer, jobRepository, parameterConverter);
}
@Test
@@ -89,30 +89,24 @@ public class JsrJobOperatorTests {
@Test
public void testNullsInConstructor() {
try {
new JsrJobOperator(null, new JobRepositorySupport(), new SimpleJobOperator(), parameterConverter);
new JsrJobOperator(null, new JobRepositorySupport(), parameterConverter);
fail("JobExplorer should be required");
} catch (IllegalArgumentException correct) {
}
try {
new JsrJobOperator(new SimpleJobExplorer(null, null, null, null), null, new SimpleJobOperator(), parameterConverter);
new JsrJobOperator(new SimpleJobExplorer(null, null, null, null), null, parameterConverter);
fail("JobRepository should be required");
} catch (IllegalArgumentException correct) {
}
try {
new JsrJobOperator(new SimpleJobExplorer(null, null, null, null), new JobRepositorySupport(), null, parameterConverter);
fail("JobOperator should be required");
} catch (IllegalArgumentException correct) {
}
try {
new JsrJobOperator(new SimpleJobExplorer(null, null, null, null), new JobRepositorySupport(), new SimpleJobOperator(), null);
new JsrJobOperator(new SimpleJobExplorer(null, null, null, null), new JobRepositorySupport(), null);
fail("ParameterConverter should be required");
} catch (IllegalArgumentException correct) {
}
new JsrJobOperator(new SimpleJobExplorer(null, null, null, null), new JobRepositorySupport(), new SimpleJobOperator(), parameterConverter);
new JsrJobOperator(new SimpleJobExplorer(null, null, null, null), new JobRepositorySupport(), parameterConverter);
}
@Test
@@ -120,16 +114,16 @@ public class JsrJobOperatorTests {
JsrJobOperator jsrJobOperatorImpl = (JsrJobOperator) jsrJobOperator;
jsrJobOperatorImpl.afterPropertiesSet();
assertNotNull(jsrJobOperatorImpl.getTaskExecutor());
assertTrue((jsrJobOperatorImpl.getTaskExecutor() instanceof SyncTaskExecutor));
assertTrue((jsrJobOperatorImpl.getTaskExecutor() instanceof AsyncTaskExecutor));
}
@Test
public void testCustomTaskExecutor() throws Exception {
JsrJobOperator jsrJobOperatorImpl = (JsrJobOperator) jsrJobOperator;
jsrJobOperatorImpl.setTaskExecutor(new SimpleAsyncTaskExecutor());
jsrJobOperatorImpl.setTaskExecutor(new SyncTaskExecutor());
jsrJobOperatorImpl.afterPropertiesSet();
assertNotNull(jsrJobOperatorImpl.getTaskExecutor());
assertTrue((jsrJobOperatorImpl.getTaskExecutor() instanceof SimpleAsyncTaskExecutor));
assertTrue((jsrJobOperatorImpl.getTaskExecutor() instanceof SyncTaskExecutor));
}
@Test
@@ -389,12 +383,10 @@ public class JsrJobOperatorTests {
}
@Test
public void testStartRoseyScenario() {
jsrJobOperator = BatchRuntime.getJobOperator();
public void testStartRoseyScenario() throws Exception {
javax.batch.runtime.JobExecution execution = runJob("jsrJobOperatorTestJob", new Properties(), TIMEOUT);
long executionId = jsrJobOperator.start("jsrJobOperatorTestJob", null);
assertEquals(BatchStatus.COMPLETED, jsrJobOperator.getJobExecution(executionId).getBatchStatus());
assertEquals(BatchStatus.COMPLETED, execution.getBatchStatus());
}
@Test
@@ -408,13 +400,13 @@ public class JsrJobOperatorTests {
} catch (NoSuchJobException ignore) {
}
long run1 = jsrJobOperator.start("jsrJobOperatorTestJob", null);
long run2 = jsrJobOperator.start("jsrJobOperatorTestJob", null);
long run3 = jsrJobOperator.start("jsrJobOperatorTestJob", null);
javax.batch.runtime.JobExecution execution1 = runJob("jsrJobOperatorTestJob", new Properties(), TIMEOUT);
javax.batch.runtime.JobExecution execution2 = runJob("jsrJobOperatorTestJob", new Properties(), TIMEOUT);
javax.batch.runtime.JobExecution execution3 = runJob("jsrJobOperatorTestJob", new Properties(), TIMEOUT);
assertEquals(BatchStatus.COMPLETED, jsrJobOperator.getJobExecution(run1).getBatchStatus());
assertEquals(BatchStatus.COMPLETED, jsrJobOperator.getJobExecution(run2).getBatchStatus());
assertEquals(BatchStatus.COMPLETED, jsrJobOperator.getJobExecution(run3).getBatchStatus());
assertEquals(BatchStatus.COMPLETED, execution1.getBatchStatus());
assertEquals(BatchStatus.COMPLETED, execution2.getBatchStatus());
assertEquals(BatchStatus.COMPLETED, execution3.getBatchStatus());
int jobInstanceCountAfter = jsrJobOperator.getJobInstanceCount("myJob3");
@@ -422,28 +414,25 @@ public class JsrJobOperatorTests {
}
@Test
public void testRestartRoseyScenario() {
jsrJobOperator = BatchRuntime.getJobOperator();
public void testRestartRoseyScenario() throws Exception {
javax.batch.runtime.JobExecution execution = runJob("jsrJobOperatorTestRestartJob", new Properties(), TIMEOUT);
long executionId = jsrJobOperator.start("jsrJobOperatorTestRestartJob", null);
assertEquals(BatchStatus.FAILED, execution.getBatchStatus());
assertEquals(BatchStatus.FAILED, jsrJobOperator.getJobExecution(executionId).getBatchStatus());
execution = restartJob(execution.getExecutionId(), null, TIMEOUT);
long finalExecutionId = jsrJobOperator.restart(executionId, null);
assertEquals(BatchStatus.COMPLETED, jsrJobOperator.getJobExecution(finalExecutionId).getBatchStatus());
assertEquals(BatchStatus.COMPLETED, execution.getBatchStatus());
}
@Test(expected = JobRestartException.class)
public void testRestartAbandoned() {
public void testRestartAbandoned() throws Exception {
jsrJobOperator = BatchRuntime.getJobOperator();
javax.batch.runtime.JobExecution execution = runJob("jsrJobOperatorTestRestartAbandonJob", null, TIMEOUT);
long executionId = jsrJobOperator.start("jsrJobOperatorTestRestartAbandonJob", null);
assertEquals(BatchStatus.FAILED, execution.getBatchStatus());
assertEquals(BatchStatus.FAILED, jsrJobOperator.getJobExecution(executionId).getBatchStatus());
jsrJobOperator.abandon(executionId);
jsrJobOperator.restart(executionId, null);
jsrJobOperator.abandon(execution.getExecutionId());
jsrJobOperator.restart(execution.getExecutionId(), null);
}
@Test
@@ -506,18 +495,43 @@ public class JsrJobOperatorTests {
fail("Should have failed");
}
@Test(expected = JobStartException.class)
public void testBeanCreationExceptionOnRestart() throws Exception {
JsrJobOperator jsrJobOperator1 = mock(JsrJobOperator.class);
when(jsrJobOperator1.restart(0l, null)).thenThrow(new JobStartException(new BeanCreationException("Bean creation exception")));
@SuppressWarnings("unchecked")
@Test(expected=JobStartException.class)
public void testStartUnableToCreateJobExecution() throws Exception {
when(jobRepository.createJobExecution("myJob", null)).thenThrow(RuntimeException.class);
try {
jsrJobOperator1.restart(0l, null);
} catch (JobStartException e) {
assertTrue(e.getCause() instanceof BeanCreationException);
throw e;
jsrJobOperator.start("myJob", null);
}
@Test
public void testJobStopRoseyScenario() throws Exception {
jsrJobOperator = BatchRuntime.getJobOperator();
long executionId = jsrJobOperator.start("longRunningJob", null);
// Give the job a chance to get started
Thread.sleep(1000l);
jsrJobOperator.stop(executionId);
// Give the job the chance to finish stopping
Thread.sleep(1000l);
assertEquals(BatchStatus.STOPPED, jsrJobOperator.getJobExecution(executionId).getBatchStatus());
}
public static class LongRunningBatchlet implements Batchlet {
private boolean stopped = false;
@Override
public String process() throws Exception {
while(!stopped) {
Thread.sleep(250);
}
return null;
}
fail("Should have failed");
@Override
public void stop() throws Exception {
stopped = true;
}
}
}