/* * Copyright 2015-2016 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 java.util.Date; import java.util.Map; import java.util.Properties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.batch.core.JobExecution; import org.springframework.batch.core.JobParameters; 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.step.tasklet.Tasklet; import org.springframework.batch.item.ExecutionContext; import org.springframework.batch.repeat.RepeatStatus; import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.channel.QueueChannel; import org.springframework.integration.support.MessageBuilder; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessagingException; import org.springframework.messaging.PollableChannel; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.xd.dirt.integration.bus.BusUtils; import org.springframework.xd.dirt.integration.bus.MessageBus; import org.springframework.xd.dirt.integration.bus.MessageBus.Capability; import org.springframework.xd.dirt.plugins.job.ExpandedJobParametersConverter; 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; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; /** * A {@link Tasklet} implementation that uses the Spring XD {@link MessageBus} to launch * jobs deployed within the same Spring XD cluster. This tasklet also receives the * results of the job from the same message bus. * * The result of the step executing this tasklet should match that of the job that was * executed by this job. So if the tasklet executes the job foo and foo returns an * {@link org.springframework.batch.core.ExitStatus} of "BAR", the ExitStatus of this step * will also be "BAR". * * @author Michael Minella * @author Gary Russell * @since 1.3.0 */ public class JobLaunchingTasklet implements Tasklet { private final Logger logger = LoggerFactory.getLogger(JobLaunchingTasklet.class); public static final String XD_ORCHESTRATION_ID = "xd_orchestration_id"; public static final String XD_PARENT_JOB_EXECUTION_ID = "xd_parent_execution_id"; private long timeout; private String jobName; private MessageBus messageBus; private JobDefinitionRepository definitionRepository; private DomainRepository<JobDefinition, String> instanceRepository; private String orchestrationId; private MessageChannel launchingChannel; private PollableChannel listeningChannel; public JobLaunchingTasklet(MessageBus messageBus, JobDefinitionRepository jobDefinitionRepository, DomainRepository<JobDefinition, String> instanceRepository, String jobName, Long timeout) { this(messageBus, jobDefinitionRepository, instanceRepository, jobName, timeout, createLaunchingChannel(jobName), createListeningChannel(jobName)); } /** * Provided for testing to be able to inject the channels for mocking. * * @param messageBus Message bus reference to launch a job and receive it's results * @param jobDefinitionRepository Repository used to look up the child job definition * @param instanceRepository Repository used to look up that the child job is deployed * @param jobName The name of the child job definition * @param launchingChannel The channel used to send the launch request * @param listeningChannel The channel used to listen for the job results */ protected JobLaunchingTasklet(MessageBus messageBus, JobDefinitionRepository jobDefinitionRepository, DomainRepository<JobDefinition, String> instanceRepository, String jobName, Long timeout, MessageChannel launchingChannel, PollableChannel listeningChannel) { Assert.notNull(messageBus, "A message bus is required"); Assert.notNull(jobDefinitionRepository, "A JobDefinitionRepository is required"); Assert.notNull(instanceRepository, "A DomainRepository is required"); Assert.notNull(jobName, "A job name is required"); this.jobName = jobName; this.messageBus = messageBus; this.definitionRepository = jobDefinitionRepository; this.instanceRepository = instanceRepository; this.launchingChannel = launchingChannel; this.listeningChannel = listeningChannel; this.timeout = timeout == null ? -1 : timeout; } private static DirectChannel createLaunchingChannel(String jobName) { DirectChannel launchingChannel = new DirectChannel(); launchingChannel.setBeanName(jobName + ":launcher"); return launchingChannel; } private static QueueChannel createListeningChannel(String jobName) { QueueChannel listeningChannel = new QueueChannel(); listeningChannel.setBeanName(jobName + ":resultListener"); return listeningChannel; } /** * Uses the {@link MessageBus} to execute a job deployed in a Spring XD cluster. * * @param contribution The contribution to the step's metrics. Not used in this case * @param chunkContext Used to get a handle on the JobExecution and JobInstance * @return RepeatStatus.FINISHED * @throws Exception */ @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { setOrchestrationId(chunkContext); String driverJobName = chunkContext.getStepContext().getStepExecution().getJobExecution().getJobInstance() .getJobName(); bindChannels(driverJobName); try { validateJobDeployment(); String jobParametersString = getJobParameters(chunkContext); logger.debug("Launching request for {} orchestration {}", this.jobName, this.orchestrationId); this.launchingChannel.send(MessageBuilder.withPayload(jobParametersString).build()); Date startTime = new Date(); long remaining = this.timeout; JobExecution results = null; while (results == null && (this.timeout > 0 ? remaining > 0 : true)) { Message<?> resultMessage = this.timeout > 0 ? this.listeningChannel.receive(remaining) : this.listeningChannel.receive(); if (resultMessage != null) { results = getResult(resultMessage); } remaining = startTime.getTime() - System.currentTimeMillis() + this.timeout; } if (results != null) { processResult(contribution, chunkContext, results); } else { throw new UnexpectedJobExecutionException("The job timed out while waiting for a result"); } logger.debug("Completed processing for {} orchestration {}", this.jobName, this.orchestrationId); return RepeatStatus.FINISHED; } finally { unbindChannels(driverJobName); } } private String getJobParameters(ChunkContext chunkContext) throws JsonProcessingException { StepExecution stepExecution = chunkContext.getStepContext().getStepExecution(); JobParameters originalJobParameters = stepExecution.getJobParameters(); Properties currentJobParameters = originalJobParameters.toProperties(); currentJobParameters.remove(ExpandedJobParametersConverter.UNIQUE_JOB_PARAMETER_KEY); String random = null; //This is a restart Map<String, Object> stepExecutionContext = chunkContext.getStepContext().getStepExecutionContext(); if(stepExecutionContext.containsKey(ExpandedJobParametersConverter.UNIQUE_JOB_PARAMETER_KEY)) { random = (String) stepExecutionContext.get(ExpandedJobParametersConverter.UNIQUE_JOB_PARAMETER_KEY); } currentJobParameters.put(XD_ORCHESTRATION_ID, this.orchestrationId); currentJobParameters.put("-" + XD_PARENT_JOB_EXECUTION_ID, stepExecution.getJobExecutionId()); if(random != null) { currentJobParameters.put(ExpandedJobParametersConverter.IS_RESTART_JOB_PARAMETER_KEY, true); currentJobParameters.put(ExpandedJobParametersConverter.UNIQUE_JOB_PARAMETER_KEY, random); } return new ObjectMapper().writeValueAsString(currentJobParameters); } private void processResult(StepContribution contribution, ChunkContext chunkContext, JobExecution results) { contribution.setExitStatus(results.getExitStatus()); if (results.getStatus().isUnsuccessful()) { chunkContext.getStepContext() .getStepExecution() .getExecutionContext() .put(ExpandedJobParametersConverter.UNIQUE_JOB_PARAMETER_KEY, results.getJobParameters().getString(ExpandedJobParametersConverter.UNIQUE_JOB_PARAMETER_KEY)); throw new UnexpectedJobExecutionException(String.format("Step failure: %s failed.", jobName)); } } private void bindChannels(String driverJobName) { messageBus.bindPubSubConsumer(getEventListenerChannelName(this.jobName, driverJobName), this.listeningChannel, null); messageBus.bindProducer("job:" + jobName, this.launchingChannel, null); } private void unbindChannels(String driverJobName) { messageBus.unbindConsumer(getEventListenerChannelName(jobName, driverJobName), this.listeningChannel); messageBus.unbindProducer("job:" + jobName, this.launchingChannel); } private void validateJobDeployment() { // Double check so that user gets an informative error message JobDefinition job = definitionRepository.findOne(jobName); if (job == null) { throw new NoSuchDefinitionException(jobName, String.format("There is no %s definition named '%%s'", "job")); } if (instanceRepository.findOne(jobName) == null) { throw new NotDeployedException(jobName, String.format("The %s named '%%s' is not currently deployed", "job")); } } private void setOrchestrationId(ChunkContext chunkContext) { this.orchestrationId = String.valueOf( chunkContext.getStepContext().getStepExecution().getJobExecution().getJobInstance().getInstanceId()); ExecutionContext stepExecutionContext = chunkContext.getStepContext().getStepExecution().getExecutionContext(); if (stepExecutionContext.containsKey(XD_ORCHESTRATION_ID)) { this.orchestrationId = (String) stepExecutionContext.get(XD_ORCHESTRATION_ID); } else { stepExecutionContext.put(XD_ORCHESTRATION_ID, this.orchestrationId); } } private String getEventListenerChannelName(String jobName, String driverJobName) { String tapName = String.format("tap:job:%s.job", jobName); if (this.messageBus.isCapable(Capability.DURABLE_PUBSUB)) { tapName = BusUtils.addGroupToPubSub(driverJobName, tapName); } return tapName; } public JobExecution getResult(Message<?> message) throws MessagingException { JobExecution jobExecution = (JobExecution) message.getPayload(); String curOrchestrationId = jobExecution.getJobParameters().getString(XD_ORCHESTRATION_ID); logger.debug("Received result for {} orchestration {}", this.jobName, curOrchestrationId); if (StringUtils.hasText(curOrchestrationId) && curOrchestrationId.equalsIgnoreCase(this.orchestrationId)) { if (!jobExecution.isRunning()) { return jobExecution; } } return null; } }