/*
* Copyright 2009-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.admin.service;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import javax.batch.operations.JobOperator;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobInstance;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersInvalidException;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.configuration.ListableJobLocator;
import org.springframework.batch.core.launch.JobExecutionNotRunningException;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.launch.NoSuchJobException;
import org.springframework.batch.core.launch.NoSuchJobExecutionException;
import org.springframework.batch.core.launch.NoSuchJobInstanceException;
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.repository.dao.ExecutionContextDao;
import org.springframework.batch.core.step.NoSuchStepException;
import org.springframework.batch.core.step.StepLocator;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.util.CollectionUtils;
/**
* Implementation of {@link JobService} that delegates most of its work to other
* off-the-shelf components.
*
* @author Dave Syer
* @author Michael Minella
*
*/
public class SimpleJobService implements JobService, DisposableBean {
private static final Log logger = LogFactory.getLog(SimpleJobService.class);
// 60 seconds
private static final int DEFAULT_SHUTDOWN_TIMEOUT = 60 * 1000;
private final SearchableJobInstanceDao jobInstanceDao;
private final SearchableJobExecutionDao jobExecutionDao;
private final JobRepository jobRepository;
private final JobLauncher jobLauncher;
private final ListableJobLocator jobLocator;
private final SearchableStepExecutionDao stepExecutionDao;
private final ExecutionContextDao executionContextDao;
private Collection<JobExecution> activeExecutions = Collections.synchronizedList(new ArrayList<JobExecution>());
private JobOperator jsrJobOperator;
private int shutdownTimeout = DEFAULT_SHUTDOWN_TIMEOUT;
/**
* Timeout for shutdown waiting for jobs to finish processing.
*
* @param shutdownTimeout in milliseconds (default 60 secs)
*/
public void setShutdownTimeout(int shutdownTimeout) {
this.shutdownTimeout = shutdownTimeout;
}
public SimpleJobService(SearchableJobInstanceDao jobInstanceDao, SearchableJobExecutionDao jobExecutionDao,
SearchableStepExecutionDao stepExecutionDao, JobRepository jobRepository, JobLauncher jobLauncher,
ListableJobLocator jobLocator, ExecutionContextDao executionContextDao) {
this(jobInstanceDao, jobExecutionDao, stepExecutionDao, jobRepository, jobLauncher, jobLocator, executionContextDao, null);
}
public SimpleJobService(SearchableJobInstanceDao jobInstanceDao, SearchableJobExecutionDao jobExecutionDao,
SearchableStepExecutionDao stepExecutionDao, JobRepository jobRepository, JobLauncher jobLauncher,
ListableJobLocator jobLocator, ExecutionContextDao executionContextDao, JobOperator jsrJobOperator) {
super();
this.jobInstanceDao = jobInstanceDao;
this.jobExecutionDao = jobExecutionDao;
this.stepExecutionDao = stepExecutionDao;
this.jobRepository = jobRepository;
this.jobLauncher = jobLauncher;
this.jobLocator = jobLocator;
this.executionContextDao = executionContextDao;
if(jsrJobOperator == null) {
logger.warn("No JobOperator compatible with JSR-352 was provided.");
}
else {
this.jsrJobOperator = jsrJobOperator;
}
}
@Override
public Collection<StepExecution> getStepExecutions(Long jobExecutionId) throws NoSuchJobExecutionException {
JobExecution jobExecution = jobExecutionDao.getJobExecution(jobExecutionId);
if (jobExecution == null) {
throw new NoSuchJobExecutionException("No JobExecution with id=" + jobExecutionId);
}
stepExecutionDao.addStepExecutions(jobExecution);
String jobName = jobExecution.getJobInstance() == null ? jobInstanceDao.getJobInstance(jobExecution).getJobName() : jobExecution.getJobInstance().getJobName();
Collection<String> missingStepNames = new LinkedHashSet<String>();
if (jobName != null) {
missingStepNames.addAll(stepExecutionDao.findStepNamesForJobExecution(jobName, "*:partition*"));
logger.debug("Found step executions in repository: " + missingStepNames);
}
Job job = null;
try {
job = jobLocator.getJob(jobName);
}
catch (NoSuchJobException e) {
// expected
}
if (job instanceof StepLocator) {
Collection<String> stepNames = ((StepLocator) job).getStepNames();
missingStepNames.addAll(stepNames);
logger.debug("Added step executions from job: " + missingStepNames);
}
for (StepExecution stepExecution : jobExecution.getStepExecutions()) {
String stepName = stepExecution.getStepName();
if (missingStepNames.contains(stepName)) {
missingStepNames.remove(stepName);
}
logger.debug("Removed step executions from job execution: " + missingStepNames);
}
for (String stepName : missingStepNames) {
StepExecution stepExecution = jobExecution.createStepExecution(stepName);
stepExecution.setStatus(BatchStatus.UNKNOWN);
}
return jobExecution.getStepExecutions();
}
@Override
public boolean isLaunchable(String jobName) {
return jobLocator.getJobNames().contains(jobName) || getJsrJobNames().contains(jobName);
}
@Override
public boolean isIncrementable(String jobName) {
try {
return jobLocator.getJobNames().contains(jobName)
&& jobLocator.getJob(jobName).getJobParametersIncrementer() != null;
}
catch (NoSuchJobException e) {
// Should not happen
throw new IllegalStateException("Unexpected non-existent job: " + jobName);
}
}
/**
* Delegates launching to {@link org.springframework.batch.admin.service.SimpleJobService#restart(Long, org.springframework.batch.core.JobParameters)}
*
* @param jobExecutionId the job execution to restart
* @return
* @throws NoSuchJobExecutionException
* @throws JobExecutionAlreadyRunningException
* @throws JobRestartException
* @throws JobInstanceAlreadyCompleteException
* @throws NoSuchJobException
* @throws JobParametersInvalidException
*/
@Override
public JobExecution restart(Long jobExecutionId) throws NoSuchJobExecutionException, JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException, NoSuchJobException, JobParametersInvalidException {
return restart(jobExecutionId, null);
}
@Override
public JobExecution restart(Long jobExecutionId, JobParameters params) throws NoSuchJobExecutionException,
JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException,
NoSuchJobException, JobParametersInvalidException {
JobExecution jobExecution = null;
JobExecution target = getJobExecution(jobExecutionId);
JobInstance lastInstance = target.getJobInstance();
if(jobLocator.getJobNames().contains(lastInstance.getJobName())) {
Job job = jobLocator.getJob(lastInstance.getJobName());
jobExecution = jobLauncher.run(job, target.getJobParameters());
if (jobExecution.isRunning()) {
activeExecutions.add(jobExecution);
}
}
else {
if(jsrJobOperator != null) {
if(params != null) {
jobExecution = new JobExecution(jsrJobOperator.restart(jobExecutionId, params.toProperties()));
}
else {
jobExecution = new JobExecution(jsrJobOperator.restart(jobExecutionId, new Properties()));
}
}
else {
throw new NoSuchJobException(String.format("Can't find job associated with job execution id %s to restart",
String.valueOf(jobExecutionId)));
}
}
return jobExecution;
}
@Override
public JobExecution launch(String jobName, JobParameters jobParameters) throws NoSuchJobException,
JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException,
JobParametersInvalidException {
JobExecution jobExecution = null;
if(jobLocator.getJobNames().contains(jobName)) {
Job job = jobLocator.getJob(jobName);
JobExecution lastJobExecution = jobRepository.getLastJobExecution(jobName, jobParameters);
boolean restart = false;
if (lastJobExecution != null) {
BatchStatus status = lastJobExecution.getStatus();
if (status.isUnsuccessful() && status!=BatchStatus.ABANDONED) {
restart = true;
}
}
if (job.getJobParametersIncrementer() != null && !restart) {
jobParameters = job.getJobParametersIncrementer().getNext(jobParameters);
}
jobExecution = jobLauncher.run(job, jobParameters);
if (jobExecution.isRunning()) {
activeExecutions.add(jobExecution);
}
}
else {
if(jsrJobOperator != null) {
jobExecution = new JobExecution(jsrJobOperator.start(jobName, jobParameters.toProperties()));
}
else {
throw new NoSuchJobException(String.format("Unable to find job %s to launch",
String.valueOf(jobName)));
}
}
return jobExecution;
}
@Override
public JobParameters getLastJobParameters(String jobName) throws NoSuchJobException {
Collection<JobExecution> executions = jobExecutionDao.getJobExecutions(jobName, 0, 1);
JobExecution lastExecution = null;
if (!CollectionUtils.isEmpty(executions)) {
lastExecution = executions.iterator().next();
}
JobParameters oldParameters = new JobParameters();
if (lastExecution != null) {
oldParameters = lastExecution.getJobParameters();
}
return oldParameters;
}
@Override
public Collection<JobExecution> listJobExecutions(int start, int count) {
return jobExecutionDao.getJobExecutions(start, count);
}
@Override
public int countJobExecutions() {
return jobExecutionDao.countJobExecutions();
}
@Override
public Collection<String> listJobs(int start, int count) {
Collection<String> jobNames = new LinkedHashSet<String>(jobLocator.getJobNames());
jobNames.addAll(getJsrJobNames());
if (start + count > jobNames.size()) {
jobNames.addAll(jobInstanceDao.getJobNames());
}
if (start >= jobNames.size()) {
start = jobNames.size();
}
if (start + count >= jobNames.size()) {
count = jobNames.size() - start;
}
return new ArrayList<String>(jobNames).subList(start, start + count);
}
private Collection<String> getJsrJobNames() {
Set<String> jsr352JobNames = new HashSet<String>();
try {
PathMatchingResourcePatternResolver pathMatchingResourcePatternResolver = new org.springframework.core.io.support.PathMatchingResourcePatternResolver();
Resource[] resources = pathMatchingResourcePatternResolver.getResources("classpath*:/META-INF/batch-jobs/**/*.xml");
for (Resource resource : resources) {
String jobXmlFileName = resource.getFilename();
jsr352JobNames.add(jobXmlFileName.substring(0, jobXmlFileName.length() - 4));
}
} catch (IOException e) {
logger.debug("Unable to list JSR-352 batch jobs", e);
}
return jsr352JobNames;
}
@Override
public int countJobs() {
Collection<String> names = new HashSet<String>(jobLocator.getJobNames());
names.addAll(jobInstanceDao.getJobNames());
return names.size();
}
@Override
public int stopAll() {
Collection<JobExecution> result = jobExecutionDao.getRunningJobExecutions();
Collection<String> jsrJobNames = getJsrJobNames();
for (JobExecution jobExecution : result) {
if(jsrJobOperator != null && jsrJobNames.contains(jobExecution.getJobInstance().getJobName())) {
jsrJobOperator.stop(jobExecution.getId());
}
else {
jobExecution.stop();
jobRepository.update(jobExecution);
}
}
return result.size();
}
@Override
public JobExecution stop(Long jobExecutionId) throws NoSuchJobExecutionException, JobExecutionNotRunningException {
JobExecution jobExecution = getJobExecution(jobExecutionId);
if (!jobExecution.isRunning()) {
throw new JobExecutionNotRunningException("JobExecution is not running and therefore cannot be stopped");
}
logger.info("Stopping job execution: " + jobExecution);
Collection<String> jsrJobNames = getJsrJobNames();
if(jsrJobOperator != null && jsrJobNames.contains(jobExecution.getJobInstance().getJobName())) {
jsrJobOperator.stop(jobExecutionId);
jobExecution = getJobExecution(jobExecutionId);
}
else {
jobExecution.stop();
jobRepository.update(jobExecution);
}
return jobExecution;
}
@Override
public JobExecution abandon(Long jobExecutionId) throws NoSuchJobExecutionException,
JobExecutionAlreadyRunningException {
JobExecution jobExecution = getJobExecution(jobExecutionId);
if (jobExecution.getStatus().isLessThan(BatchStatus.STOPPING)) {
throw new JobExecutionAlreadyRunningException(
"JobExecution is running or complete and therefore cannot be aborted");
}
logger.info("Aborting job execution: " + jobExecution);
Collection<String> jsrJobNames = getJsrJobNames();
JobInstance jobInstance = jobExecution.getJobInstance();
if(jsrJobOperator != null && jsrJobNames.contains(jobInstance.getJobName())) {
jsrJobOperator.abandon(jobExecutionId);
jobExecution = getJobExecution(jobExecutionId);
}
else {
jobExecution.upgradeStatus(BatchStatus.ABANDONED);
jobExecution.setEndTime(new Date());
jobRepository.update(jobExecution);
}
return jobExecution;
}
@Override
public int countJobExecutionsForJob(String name) throws NoSuchJobException {
checkJobExists(name);
return jobExecutionDao.countJobExecutions(name);
}
@Override
public int countJobInstances(String name) throws NoSuchJobException {
return jobInstanceDao.countJobInstances(name);
}
@Override
public JobExecution getJobExecution(Long jobExecutionId) throws NoSuchJobExecutionException {
JobExecution jobExecution = jobExecutionDao.getJobExecution(jobExecutionId);
if (jobExecution == null) {
throw new NoSuchJobExecutionException("There is no JobExecution with id=" + jobExecutionId);
}
jobExecution.setJobInstance(jobInstanceDao.getJobInstance(jobExecution));
try {
jobExecution.setExecutionContext(executionContextDao.getExecutionContext(jobExecution));
}
catch (Exception e) {
logger.info("Cannot load execution context for job execution: " + jobExecution);
}
stepExecutionDao.addStepExecutions(jobExecution);
return jobExecution;
}
@Override
public Collection<JobExecution> getJobExecutionsForJobInstance(String name, Long jobInstanceId)
throws NoSuchJobException {
checkJobExists(name);
List<JobExecution> jobExecutions = jobExecutionDao.findJobExecutions(jobInstanceDao
.getJobInstance(jobInstanceId));
for (JobExecution jobExecution : jobExecutions) {
stepExecutionDao.addStepExecutions(jobExecution);
}
return jobExecutions;
}
@Override
public StepExecution getStepExecution(Long jobExecutionId, Long stepExecutionId)
throws NoSuchJobExecutionException, NoSuchStepExecutionException {
JobExecution jobExecution = getJobExecution(jobExecutionId);
StepExecution stepExecution = stepExecutionDao.getStepExecution(jobExecution, stepExecutionId);
if (stepExecution == null) {
throw new NoSuchStepExecutionException("There is no StepExecution with jobExecutionId=" + jobExecutionId
+ " and id=" + stepExecutionId);
}
try {
stepExecution.setExecutionContext(executionContextDao.getExecutionContext(stepExecution));
}
catch (Exception e) {
logger.info("Cannot load execution context for step execution: " + stepExecution);
}
return stepExecution;
}
@Override
public Collection<JobExecution> listJobExecutionsForJob(String jobName, int start, int count)
throws NoSuchJobException {
checkJobExists(jobName);
List<JobExecution> jobExecutions = jobExecutionDao.getJobExecutions(jobName, start, count);
for (JobExecution jobExecution : jobExecutions) {
stepExecutionDao.addStepExecutions(jobExecution);
}
return jobExecutions;
}
@Override
public Collection<StepExecution> listStepExecutionsForStep(String jobName, String stepName, int start, int count)
throws NoSuchStepException {
if (stepExecutionDao.countStepExecutions(jobName, stepName) == 0) {
throw new NoSuchStepException("No step executions exist with this step name: " + stepName);
}
return stepExecutionDao.findStepExecutions(jobName, stepName, start, count);
}
@Override
public int countStepExecutionsForStep(String jobName, String stepName) throws NoSuchStepException {
return stepExecutionDao.countStepExecutions(jobName, stepName);
}
@Override
public JobInstance getJobInstance(long jobInstanceId) throws NoSuchJobInstanceException {
JobInstance jobInstance = jobInstanceDao.getJobInstance(jobInstanceId);
if (jobInstance == null) {
throw new NoSuchJobInstanceException("JobInstance with id=" + jobInstanceId + " does not exist");
}
return jobInstance;
}
@Override
public Collection<JobInstance> listJobInstances(String jobName, int start, int count) throws NoSuchJobException {
checkJobExists(jobName);
return jobInstanceDao.getJobInstances(jobName, start, count);
}
@Override
public Collection<String> getStepNamesForJob(String jobName) throws NoSuchJobException {
try {
Job job = jobLocator.getJob(jobName);
if (job instanceof StepLocator) {
return ((StepLocator) job).getStepNames();
}
}
catch (NoSuchJobException e) {
// ignore
}
Collection<String> stepNames = new LinkedHashSet<String>();
for (JobExecution jobExecution : listJobExecutionsForJob(jobName, 0, 100)) {
for (StepExecution stepExecution : jobExecution.getStepExecutions()) {
stepNames.add(stepExecution.getStepName());
}
}
return Collections.unmodifiableList(new ArrayList<String>(stepNames));
}
private void checkJobExists(String jobName) throws NoSuchJobException {
if(getJsrJobNames().contains(jobName)) {
return;
}
if (jobLocator.getJobNames().contains(jobName)) {
return;
}
if (jobInstanceDao.countJobInstances(jobName) > 0) {
return;
}
throw new NoSuchJobException("No Job with that name either current or historic: [" + jobName + "]");
}
/**
* Stop all the active jobs and wait for them (up to a time out) to finish
* processing.
*/
@Override
public void destroy() throws Exception {
Exception firstException = null;
for (JobExecution jobExecution : activeExecutions) {
try {
if (jobExecution.isRunning()) {
stop(jobExecution.getId());
}
}
catch (JobExecutionNotRunningException e) {
logger.info("JobExecution is not running so it cannot be stopped");
}
catch (Exception e) {
logger.error("Unexpected exception stopping JobExecution", e);
if (firstException == null) {
firstException = e;
}
}
}
int count = 0;
int maxCount = (shutdownTimeout + 1000) / 1000;
while (!activeExecutions.isEmpty() && ++count < maxCount) {
logger.error("Waiting for " + activeExecutions.size() + " active executions to complete");
removeInactiveExecutions();
Thread.sleep(1000L);
}
if (firstException != null) {
throw firstException;
}
}
/**
* Check all the active executions and see if they are still actually
* running. Remove the ones that have completed.
*/
@Scheduled(fixedDelay = 60000)
public void removeInactiveExecutions() {
for (Iterator<JobExecution> iterator = activeExecutions.iterator(); iterator.hasNext();) {
JobExecution jobExecution = iterator.next();
try {
jobExecution = getJobExecution(jobExecution.getId());
}
catch (NoSuchJobExecutionException e) {
logger.error("Unexpected exception loading JobExecution", e);
}
if (!jobExecution.isRunning()) {
iterator.remove();
}
}
}
}