/*
* Copyright 2015 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.xd.dirt.batch.tasklet;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isNull;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Date;
import java.util.Properties;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobInstance;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.UnexpectedJobExecutionException;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.scope.context.StepContext;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.TaskExecutor;
import org.springframework.integration.amqp.channel.PublishSubscribeAmqpChannel;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.xd.dirt.integration.bus.MessageBus;
import org.springframework.xd.dirt.stream.JobDefinition;
import org.springframework.xd.dirt.stream.JobDefinitionRepository;
import org.springframework.xd.dirt.stream.NoSuchDefinitionException;
import org.springframework.xd.dirt.stream.NotDeployedException;
import org.springframework.xd.store.DomainRepository;
/**
* @author Michael Minella
* @author Gary Russell
*/
public class JobLaunchingTaskletTests {
private JobLaunchingTasklet tasklet;
@Mock
private MessageBus bus;
@Mock
private JobDefinitionRepository jobDefinitionRepository;
@Mock
private DomainRepository<JobDefinition, String> instanceRepository;
@Mock
private MessageChannel launchingChannel;
@Spy
private PollableChannel listeningChannel = new QueueChannel();
private TaskExecutor taskExecutor = new SimpleAsyncTaskExecutor();
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
this.tasklet = new JobLaunchingTasklet(bus, jobDefinitionRepository, instanceRepository, "foo", null,
launchingChannel, listeningChannel);
}
@Test
public void testInitialLaunch() throws Exception {
JobInstance jobInstance = new JobInstance(3l, "masterFoo");
JobExecution jobExecution = new JobExecution(5l);
jobExecution.setJobInstance(jobInstance);
StepExecution stepExecution = new StepExecution("masterFoo", jobExecution, 7l);
final StepContribution stepContribution = new StepContribution(stepExecution);
final ChunkContext chunkContext = new ChunkContext(new StepContext(stepExecution));
JobDefinition jobDefinition = new JobDefinition("foo", "foo");
when(jobDefinitionRepository.findOne("foo")).thenReturn(jobDefinition);
when(instanceRepository.findOne("foo")).thenReturn(jobDefinition);
doReturn(true).when(launchingChannel).send(any(Message.class));
FutureTask<RepeatStatus> result = new FutureTask<RepeatStatus>(new Callable<RepeatStatus>() {
@Override
public RepeatStatus call() throws Exception {
return tasklet.execute(stepContribution, chunkContext);
}
});
taskExecutor.execute(result);
JobParameters jobParameters = new JobParametersBuilder().addString(JobLaunchingTasklet.XD_ORCHESTRATION_ID,
"3").toJobParameters();
JobExecution slaveExecution = new JobExecution(9l, jobParameters);
slaveExecution.setStatus(BatchStatus.COMPLETED);
slaveExecution.setEndTime(new Date());
this.listeningChannel.send(MessageBuilder.withPayload(slaveExecution).build());
assertEquals(result.get(5, TimeUnit.SECONDS), RepeatStatus.FINISHED);
assertEquals(stepExecution.getExecutionContext().get(JobLaunchingTasklet.XD_ORCHESTRATION_ID), "3");
verify(bus).bindPubSubConsumer(eq("tap:job:foo.job"), any(PublishSubscribeAmqpChannel.class),
(Properties) isNull());
}
@Test
public void testInitialLaunchWithOneJobParameter()
throws ExecutionException, InterruptedException, TimeoutException {
JobParameters originalJobParameters = new JobParametersBuilder().addString("bar", "baz").toJobParameters();
JobInstance jobInstance = new JobInstance(3l, "masterFoo");
JobExecution jobExecution = new JobExecution(5l, originalJobParameters);
jobExecution.setJobInstance(jobInstance);
StepExecution stepExecution = new StepExecution("masterFoo", jobExecution, 7l);
final StepContribution stepContribution = new StepContribution(stepExecution);
final ChunkContext chunkContext = new ChunkContext(new StepContext(stepExecution));
JobDefinition jobDefinition = new JobDefinition("foo", "foo");
when(jobDefinitionRepository.findOne("foo")).thenReturn(jobDefinition);
when(instanceRepository.findOne("foo")).thenReturn(jobDefinition);
doReturn(true).when(launchingChannel).send(any(Message.class));
FutureTask<RepeatStatus> result = new FutureTask<RepeatStatus>(new Callable<RepeatStatus>() {
@Override
public RepeatStatus call() throws Exception {
assertEquals(chunkContext.getStepContext().getStepExecution().getJobParameters().getString("bar"),
"baz");
return tasklet.execute(stepContribution, chunkContext);
}
});
taskExecutor.execute(result);
JobParameters jobParameters = new JobParametersBuilder().addString(JobLaunchingTasklet.XD_ORCHESTRATION_ID,
"3").toJobParameters();
JobExecution slaveExecution = new JobExecution(9l, jobParameters);
slaveExecution.setStatus(BatchStatus.COMPLETED);
slaveExecution.setEndTime(new Date());
//This is to allow the job to "start" before mocking the sending of the event
Thread.sleep(1000l);
this.listeningChannel.send(MessageBuilder.withPayload(slaveExecution).build());
assertEquals(result.get(5, TimeUnit.SECONDS), RepeatStatus.FINISHED);
assertEquals(stepExecution.getExecutionContext().get(JobLaunchingTasklet.XD_ORCHESTRATION_ID), "3");
verify(bus).bindPubSubConsumer(eq("tap:job:foo.job"), any(PublishSubscribeAmqpChannel.class),
(Properties) isNull());
}
@Test
public void testInitialLaunchWithThreeJobParameters()
throws ExecutionException, InterruptedException, TimeoutException {
JobParameters originalJobParameters = new JobParametersBuilder()
.addString("string", "baz")
.addLong("long", 5l)
.addDate("date", new Date(1l))
.toJobParameters();
JobInstance jobInstance = new JobInstance(3l, "masterFoo");
JobExecution jobExecution = new JobExecution(5l, originalJobParameters);
jobExecution.setJobInstance(jobInstance);
StepExecution stepExecution = new StepExecution("masterFoo", jobExecution, 7l);
final StepContribution stepContribution = new StepContribution(stepExecution);
final ChunkContext chunkContext = new ChunkContext(new StepContext(stepExecution));
JobDefinition jobDefinition = new JobDefinition("foo", "foo");
when(jobDefinitionRepository.findOne("foo")).thenReturn(jobDefinition);
when(instanceRepository.findOne("foo")).thenReturn(jobDefinition);
doReturn(true).when(launchingChannel).send(any(Message.class));
FutureTask<RepeatStatus> result = new FutureTask<RepeatStatus>(new Callable<RepeatStatus>() {
@Override
public RepeatStatus call() throws Exception {
JobParameters jobParameters = chunkContext.getStepContext().getStepExecution().getJobParameters();
assertEquals(jobParameters.getString("string"), "baz");
assertEquals((long) jobParameters.getLong("long"), 5l);
assertEquals(jobParameters.getDate("date"), new Date(1));
return tasklet.execute(stepContribution, chunkContext);
}
});
taskExecutor.execute(result);
JobParameters jobParameters = new JobParametersBuilder().addString(JobLaunchingTasklet.XD_ORCHESTRATION_ID,
"3").toJobParameters();
JobExecution slaveExecution = new JobExecution(9l, jobParameters);
slaveExecution.setStatus(BatchStatus.COMPLETED);
slaveExecution.setEndTime(new Date());
this.listeningChannel.send(MessageBuilder.withPayload(slaveExecution).build());
assertEquals(result.get(5, TimeUnit.SECONDS), RepeatStatus.FINISHED);
assertEquals(stepExecution.getExecutionContext().get(JobLaunchingTasklet.XD_ORCHESTRATION_ID), "3");
verify(bus).bindPubSubConsumer(eq("tap:job:foo.job"), any(PublishSubscribeAmqpChannel.class),
(Properties) isNull());
}
@Test
public void testJobNotDefined() throws InterruptedException, ExecutionException, TimeoutException {
JobInstance jobInstance = new JobInstance(3l, "masterFoo");
JobExecution jobExecution = new JobExecution(5l);
jobExecution.setJobInstance(jobInstance);
StepExecution stepExecution = new StepExecution("masterFoo", jobExecution, 7l);
final StepContribution stepContribution = new StepContribution(stepExecution);
final ChunkContext chunkContext = new ChunkContext(new StepContext(stepExecution));
FutureTask<RepeatStatus> result = new FutureTask<RepeatStatus>(new Callable<RepeatStatus>() {
@Override
public RepeatStatus call() throws Exception {
return tasklet.execute(stepContribution, chunkContext);
}
});
taskExecutor.execute(result);
try {
result.get(5, TimeUnit.SECONDS);
}
catch (ExecutionException e) {
Throwable cause = e.getCause();
assertEquals(cause.getClass(), NoSuchDefinitionException.class);
assertEquals(cause.getMessage(), "There is no job definition named 'foo'");
}
}
@Test
public void testJobNotDeployed() throws TimeoutException, InterruptedException {
JobInstance jobInstance = new JobInstance(3l, "masterFoo");
JobExecution jobExecution = new JobExecution(5l);
jobExecution.setJobInstance(jobInstance);
StepExecution stepExecution = new StepExecution("masterFoo", jobExecution, 7l);
final StepContribution stepContribution = new StepContribution(stepExecution);
final ChunkContext chunkContext = new ChunkContext(new StepContext(stepExecution));
JobDefinition jobDefinition = new JobDefinition("foo", "foo");
when(jobDefinitionRepository.findOne("foo")).thenReturn(jobDefinition);
FutureTask<RepeatStatus> result = new FutureTask<RepeatStatus>(new Callable<RepeatStatus>() {
@Override
public RepeatStatus call() throws Exception {
return tasklet.execute(stepContribution, chunkContext);
}
});
taskExecutor.execute(result);
try {
result.get(5, TimeUnit.SECONDS);
}
catch (ExecutionException e) {
Throwable cause = e.getCause();
assertEquals(cause.getClass(), NotDeployedException.class);
assertEquals(cause.getMessage(), "The job named 'foo' is not currently deployed");
}
}
@Test
public void testJobFailed() throws InterruptedException, ExecutionException, TimeoutException {
JobInstance jobInstance = new JobInstance(3l, "masterFoo");
JobExecution jobExecution = new JobExecution(5l);
jobExecution.setJobInstance(jobInstance);
StepExecution stepExecution = new StepExecution("masterFoo", jobExecution, 7l);
final StepContribution stepContribution = new StepContribution(stepExecution);
final ChunkContext chunkContext = new ChunkContext(new StepContext(stepExecution));
JobDefinition jobDefinition = new JobDefinition("foo", "foo");
when(jobDefinitionRepository.findOne("foo")).thenReturn(jobDefinition);
when(instanceRepository.findOne("foo")).thenReturn(jobDefinition);
FutureTask<RepeatStatus> result = new FutureTask<RepeatStatus>(new Callable<RepeatStatus>() {
@Override
public RepeatStatus call() throws Exception {
return tasklet.execute(stepContribution, chunkContext);
}
});
taskExecutor.execute(result);
JobParameters jobParameters = new JobParametersBuilder().addString(JobLaunchingTasklet.XD_ORCHESTRATION_ID,
"3").toJobParameters();
JobExecution slaveExecution = new JobExecution(9l, jobParameters);
slaveExecution.setStatus(BatchStatus.FAILED);
slaveExecution.setExitStatus(new ExitStatus("Job Failure"));
slaveExecution.setEndTime(new Date());
//This is to allow the job to "start" before mocking the sending of the event
Thread.sleep(1000l);
this.listeningChannel.send(MessageBuilder.withPayload(slaveExecution).build());
try {
assertEquals(result.get(5, TimeUnit.SECONDS), RepeatStatus.FINISHED);
}
catch (ExecutionException e) {
Throwable cause = e.getCause();
assertEquals(cause.getClass(), UnexpectedJobExecutionException.class);
assertEquals(cause.getMessage(), "Step failure: foo failed.");
}
assertEquals(stepExecution.getExecutionContext().get(JobLaunchingTasklet.XD_ORCHESTRATION_ID), "3");
verify(bus).bindPubSubConsumer(eq("tap:job:foo.job"), any(PublishSubscribeAmqpChannel.class),
(Properties) isNull());
}
@Test
public void testRestart() throws InterruptedException, ExecutionException, TimeoutException {
JobInstance jobInstance = new JobInstance(3l, "masterFoo");
JobExecution jobExecution = new JobExecution(5l);
jobExecution.setJobInstance(jobInstance);
StepExecution stepExecution = new StepExecution("masterFoo", jobExecution, 7l);
stepExecution.getExecutionContext().put(JobLaunchingTasklet.XD_ORCHESTRATION_ID, "3");
final StepContribution stepContribution = new StepContribution(stepExecution);
final ChunkContext chunkContext = new ChunkContext(new StepContext(stepExecution));
JobDefinition jobDefinition = new JobDefinition("foo", "foo");
when(jobDefinitionRepository.findOne("foo")).thenReturn(jobDefinition);
when(instanceRepository.findOne("foo")).thenReturn(jobDefinition);
doReturn(true).when(launchingChannel).send(any(Message.class));
FutureTask<RepeatStatus> result = new FutureTask<RepeatStatus>(new Callable<RepeatStatus>() {
@Override
public RepeatStatus call() throws Exception {
return tasklet.execute(stepContribution, chunkContext);
}
});
taskExecutor.execute(result);
JobParameters jobParameters = new JobParametersBuilder().addString(JobLaunchingTasklet.XD_ORCHESTRATION_ID,
"3").toJobParameters();
JobExecution slaveExecution = new JobExecution(9l, jobParameters);
slaveExecution.setStatus(BatchStatus.COMPLETED);
slaveExecution.setEndTime(new Date());
//This is to allow the job to "start" before mocking the sending of the event
Thread.sleep(1000l);
this.listeningChannel.send(MessageBuilder.withPayload(slaveExecution).build());
assertEquals(result.get(5, TimeUnit.SECONDS), RepeatStatus.FINISHED);
assertEquals(stepExecution.getExecutionContext().get(JobLaunchingTasklet.XD_ORCHESTRATION_ID), "3");
verify(bus).bindPubSubConsumer(eq("tap:job:foo.job"), any(PublishSubscribeAmqpChannel.class),
(Properties) isNull());
}
@Test
public void testTimeout() throws Exception {
this.tasklet = new JobLaunchingTasklet(bus, jobDefinitionRepository, instanceRepository, "foo", 2000l,
launchingChannel, listeningChannel);
JobInstance jobInstance = new JobInstance(3l, "masterFoo");
JobExecution jobExecution = new JobExecution(5l);
jobExecution.setJobInstance(jobInstance);
StepExecution stepExecution = new StepExecution("masterFoo", jobExecution, 7l);
final StepContribution stepContribution = new StepContribution(stepExecution);
final ChunkContext chunkContext = new ChunkContext(new StepContext(stepExecution));
JobDefinition jobDefinition = new JobDefinition("foo", "foo");
when(jobDefinitionRepository.findOne("foo")).thenReturn(jobDefinition);
when(instanceRepository.findOne("foo")).thenReturn(jobDefinition);
doReturn(true).when(launchingChannel).send(any(Message.class));
FutureTask<RepeatStatus> result = new FutureTask<RepeatStatus>(new Callable<RepeatStatus>() {
@Override
public RepeatStatus call() throws Exception {
return tasklet.execute(stepContribution, chunkContext);
}
});
taskExecutor.execute(result);
try {
result.get(5, TimeUnit.SECONDS);
fail();
}
catch (ExecutionException e) {
assertEquals(UnexpectedJobExecutionException.class, e.getCause().getClass());
assertEquals("The job timed out while waiting for a result", e.getCause().getMessage());
}
}
}