From cd4dbe6a93e4553b965da204fc83eb5d1cd4596c Mon Sep 17 00:00:00 2001 From: Glenn Renfro Date: Mon, 22 Jan 2018 10:53:36 -0500 Subject: [PATCH] Allows failed batch apps to set exit code of task app. resolves #201 Please enter the commit message for your changes. Lines starting Cleanup Removed taskProperties from TaskBatchAutoConfiguration --- spring-cloud-task-batch/pom.xml | 9 + .../TaskBatchAutoConfiguration.java | 10 +- .../configuration/TaskBatchProperties.java | 43 ++++ .../TaskJobLauncherAutoConfiguration.java | 53 +++++ ...bLauncherCommandLineRunnerFactoryBean.java | 74 ++++++ .../TaskJobLauncherCommandLineRunner.java | 192 ++++++++++++++++ .../main/resources/META-INF/spring.factories | 2 +- ...TaskJobLauncherAutoConfigurationTests.java | 60 +++++ ...JobLauncherCommandLineRunnerCoreTests.java | 210 +++++++++++++++++ ...TaskJobLauncherCommandLineRunnerTests.java | 213 ++++++++++++++++++ .../TaskBatchExecutionListenerTests.java | 2 +- .../src/main/asciidoc/batch.adoc | 15 +- 12 files changed, 870 insertions(+), 13 deletions(-) create mode 100644 spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskBatchProperties.java create mode 100644 spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskJobLauncherAutoConfiguration.java create mode 100644 spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskJobLauncherCommandLineRunnerFactoryBean.java create mode 100644 spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/handler/TaskJobLauncherCommandLineRunner.java create mode 100644 spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/configuration/TaskJobLauncherAutoConfigurationTests.java create mode 100644 spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/handler/TaskJobLauncherCommandLineRunnerCoreTests.java create mode 100644 spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/handler/TaskJobLauncherCommandLineRunnerTests.java diff --git a/spring-cloud-task-batch/pom.xml b/spring-cloud-task-batch/pom.xml index 9d542a4a..9822728b 100644 --- a/spring-cloud-task-batch/pom.xml +++ b/spring-cloud-task-batch/pom.xml @@ -70,5 +70,14 @@ spring-boot-configuration-processor true + + org.assertj + assertj-core + test + + + org.springframework.boot + spring-boot-test + diff --git a/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskBatchAutoConfiguration.java b/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskBatchAutoConfiguration.java index e76439f2..bae7d5c3 100644 --- a/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskBatchAutoConfiguration.java +++ b/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskBatchAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2016-2018 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. @@ -56,14 +56,11 @@ public class TaskBatchAutoConfiguration { @Autowired private ApplicationContext context; - @Autowired - private TaskProperties taskProperties; - @Bean public TaskBatchExecutionListenerFactoryBean taskBatchExecutionListener(TaskExplorer taskExplorer) { TaskConfigurer taskConfigurer = null; - if(!context.getBeansOfType(TaskConfigurer.class).isEmpty()) { - taskConfigurer = context.getBean(TaskConfigurer.class); + if(!this.context.getBeansOfType(TaskConfigurer.class).isEmpty()) { + taskConfigurer = this.context.getBean(TaskConfigurer.class); } if(taskConfigurer != null && taskConfigurer.getTaskDataSource() != null) { return new TaskBatchExecutionListenerFactoryBean( @@ -74,6 +71,5 @@ public class TaskBatchAutoConfiguration { return new TaskBatchExecutionListenerFactoryBean(null, taskExplorer); } } - } } diff --git a/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskBatchProperties.java b/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskBatchProperties.java new file mode 100644 index 00000000..ebbed8e6 --- /dev/null +++ b/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskBatchProperties.java @@ -0,0 +1,43 @@ +/* + * Copyright 2018 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.cloud.task.batch.configuration; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Establish properties to be used for how Tasks work with + * Spring Batch. + * + * @author Glenn Renfro + */ +@ConfigurationProperties(prefix = "spring.cloud.task.batch") +public class TaskBatchProperties { + + /** + * Comma-separated list of job names to execute on startup (for instance, + * `job1,job2`). By default, all Jobs found in the context are executed. + */ + private String jobNames = ""; + + public String getJobNames() { + return this.jobNames; + } + + public void setJobNames(String jobNames) { + this.jobNames = jobNames; + } +} diff --git a/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskJobLauncherAutoConfiguration.java b/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskJobLauncherAutoConfiguration.java new file mode 100644 index 00000000..8f811783 --- /dev/null +++ b/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskJobLauncherAutoConfiguration.java @@ -0,0 +1,53 @@ +/* + * Copyright 2018 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.cloud.task.batch.configuration; + +import java.util.List; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.configuration.JobRegistry; +import org.springframework.batch.core.explore.JobExplorer; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Provides auto configuration for the + * {@link org.springframework.cloud.task.batch.handler.TaskJobLauncherCommandLineRunner}. + * + * @author Glenn Renfro + */ +@Configuration +@ConditionalOnProperty(name = "spring.cloud.task.batch.commandLineRunnerEnabled", havingValue = "true", matchIfMissing = false) +@EnableConfigurationProperties(TaskBatchProperties.class) +public class TaskJobLauncherAutoConfiguration { + + @Autowired + private TaskBatchProperties properties; + + @Bean + public TaskJobLauncherCommandLineRunnerFactoryBean jobLauncherCommandLineRunner(JobLauncher jobLauncher, + JobExplorer jobExplorer, List jobs, JobRegistry jobRegistry) { + TaskJobLauncherCommandLineRunnerFactoryBean taskJobLauncherCommandLineRunner = new TaskJobLauncherCommandLineRunnerFactoryBean( + jobLauncher, jobExplorer, jobs, this.properties.getJobNames(), jobRegistry); + return taskJobLauncherCommandLineRunner; + } +} diff --git a/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskJobLauncherCommandLineRunnerFactoryBean.java b/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskJobLauncherCommandLineRunnerFactoryBean.java new file mode 100644 index 00000000..3e209148 --- /dev/null +++ b/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskJobLauncherCommandLineRunnerFactoryBean.java @@ -0,0 +1,74 @@ +/* + * Copyright 2018 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.cloud.task.batch.configuration; + +import java.util.List; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.configuration.JobRegistry; +import org.springframework.batch.core.explore.JobExplorer; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.cloud.task.batch.handler.TaskJobLauncherCommandLineRunner; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Factory bean for creating an instance of {@link TaskJobLauncherCommandLineRunner}. + * + * @author Glenn Renfro + */ +public class TaskJobLauncherCommandLineRunnerFactoryBean implements FactoryBean { + + private JobLauncher jobLauncher; + + private JobExplorer jobExplorer; + + private List jobs; + + private String jobNames; + + private JobRegistry jobRegistry; + + public TaskJobLauncherCommandLineRunnerFactoryBean(JobLauncher jobLauncher, + JobExplorer jobExplorer, List jobs, String jobNames, + JobRegistry jobRegistry) { + this.jobLauncher = jobLauncher; + this.jobExplorer = jobExplorer; + Assert.notEmpty(jobs, "jobs must not be null nor empty"); + this.jobs = jobs; + this.jobNames = jobNames; + this.jobRegistry = jobRegistry; + } + + @Override + public TaskJobLauncherCommandLineRunner getObject() throws Exception { + TaskJobLauncherCommandLineRunner taskJobLauncherCommandLineRunner = + new TaskJobLauncherCommandLineRunner(this.jobLauncher, this.jobExplorer); + taskJobLauncherCommandLineRunner.setJobs(this.jobs); + if(StringUtils.hasText(this.jobNames)) { + taskJobLauncherCommandLineRunner.setJobNames(this.jobNames); + } + taskJobLauncherCommandLineRunner.setJobRegistry(this.jobRegistry); + return taskJobLauncherCommandLineRunner; + } + + @Override + public Class getObjectType() { + return TaskJobLauncherCommandLineRunner.class; + } +} diff --git a/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/handler/TaskJobLauncherCommandLineRunner.java b/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/handler/TaskJobLauncherCommandLineRunner.java new file mode 100644 index 00000000..fb3c54a7 --- /dev/null +++ b/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/handler/TaskJobLauncherCommandLineRunner.java @@ -0,0 +1,192 @@ +/* + * Copyright 2018 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.cloud.task.batch.handler; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Properties; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobExecutionException; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.JobParametersInvalidException; +import org.springframework.batch.core.configuration.JobRegistry; +import org.springframework.batch.core.converter.DefaultJobParametersConverter; +import org.springframework.batch.core.converter.JobParametersConverter; +import org.springframework.batch.core.explore.JobExplorer; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.launch.JobParametersNotFoundException; +import org.springframework.batch.core.launch.NoSuchJobException; +import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException; +import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException; +import org.springframework.batch.core.repository.JobRestartException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.autoconfigure.batch.JobExecutionEvent; +import org.springframework.cloud.task.listener.TaskException; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.core.Ordered; +import org.springframework.util.PatternMatchUtils; +import org.springframework.util.StringUtils; + +/** + * {@link CommandLineRunner} to {@link JobLauncher launch} Spring Batch jobs. Runs all + * jobs in the surrounding context by default and throw an exception upon the + * first job that returns an {@link ExitStatus} of FAILED. + * Can also be used to launch a specific job by providing a jobName. The + * TaskJobLaunchercommandLineRunner takes the place of the + * {@link org.springframework.boot.autoconfigure.batch.JobLauncherCommandLineRunner} + * when it is in use. + * + * @author Glenn Renfro + * @since 2.0.0 + */ +public class TaskJobLauncherCommandLineRunner implements CommandLineRunner, Ordered, ApplicationEventPublisherAware{ + /** + * The default order for the command line runner. + */ + public static final int DEFAULT_ORDER = 0; + + private static final Log logger = LogFactory + .getLog(TaskJobLauncherCommandLineRunner.class); + + private JobParametersConverter converter = new DefaultJobParametersConverter(); + + private JobLauncher jobLauncher; + + private JobRegistry jobRegistry; + + private JobExplorer jobExplorer; + + private String jobNames; + + private Collection jobs = Collections.emptySet(); + + private int order = DEFAULT_ORDER; + + private ApplicationEventPublisher publisher; + + public TaskJobLauncherCommandLineRunner(JobLauncher jobLauncher, + JobExplorer jobExplorer) { + this.jobLauncher = jobLauncher; + this.jobExplorer = jobExplorer; + } + + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return this.order; + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { + this.publisher = publisher; + } + + public void setJobRegistry(JobRegistry jobRegistry) { + this.jobRegistry = jobRegistry; + } + + public void setJobNames(String jobNames) { + this.jobNames = jobNames; + } + + public void setJobParametersConverter(JobParametersConverter converter) { + this.converter = converter; + } + + public void setJobs(Collection jobs) { + this.jobs = jobs; + } + + @Override + public void run(String... args) throws JobExecutionException { + logger.info("Running default command line with: " + Arrays.asList(args)); + launchJobFromProperties(StringUtils.splitArrayElementsIntoProperties(args, "=")); + } + + protected void launchJobFromProperties(Properties properties) + throws JobExecutionException { + JobParameters jobParameters = this.converter.getJobParameters(properties); + executeLocalJobs(jobParameters); + executeRegisteredJobs(jobParameters); + } + + private void executeRegisteredJobs(JobParameters jobParameters) + throws JobExecutionException { + if (this.jobRegistry != null && StringUtils.hasText(this.jobNames)) { + String[] jobsToRun = this.jobNames.split(","); + for (String jobName : jobsToRun) { + try { + Job job = this.jobRegistry.getJob(jobName); + if (this.jobs.contains(job)) { + continue; + } + execute(job, jobParameters); + } + catch (NoSuchJobException ex) { + logger.debug("No job found in registry for job name: " + jobName); + } + } + } + } + + protected void execute(Job job, JobParameters jobParameters) + throws JobExecutionAlreadyRunningException, JobRestartException, + JobInstanceAlreadyCompleteException, JobParametersInvalidException, + JobParametersNotFoundException { + JobParameters nextParameters = new JobParametersBuilder(jobParameters, + this.jobExplorer).getNextJobParameters(job).toJobParameters(); + JobExecution execution = this.jobLauncher.run(job, nextParameters); + if (this.publisher != null) { + this.publisher.publishEvent(new JobExecutionEvent(execution)); + } + if(execution.getExitStatus().getExitCode().equals(ExitStatus.FAILED.getExitCode())) { + String message = String.format("Job %s failed during " + + "execution for jobId %s with jobExecutionId of %s", + execution.getJobInstance().getJobName(), + execution.getJobId(), execution.getId()); + logger.error(message); + throw new TaskException(message); + } + } + + private void executeLocalJobs(JobParameters jobParameters) + throws JobExecutionException { + for (Job job : this.jobs) { + if (StringUtils.hasText(this.jobNames)) { + String[] jobsToRun = this.jobNames.split(","); + if (!PatternMatchUtils.simpleMatch(jobsToRun, job.getName())) { + logger.debug("Skipped job: " + job.getName()); + continue; + } + } + execute(job, jobParameters); + } + } +} diff --git a/spring-cloud-task-batch/src/main/resources/META-INF/spring.factories b/spring-cloud-task-batch/src/main/resources/META-INF/spring.factories index 262efdef..3db9c9fe 100644 --- a/spring-cloud-task-batch/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-task-batch/src/main/resources/META-INF/spring.factories @@ -1 +1 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.springframework.cloud.task.batch.configuration.TaskBatchAutoConfiguration +org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.springframework.cloud.task.batch.configuration.TaskBatchAutoConfiguration,org.springframework.cloud.task.batch.configuration.TaskJobLauncherAutoConfiguration diff --git a/spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/configuration/TaskJobLauncherAutoConfigurationTests.java b/spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/configuration/TaskJobLauncherAutoConfigurationTests.java new file mode 100644 index 00000000..83e3a9e4 --- /dev/null +++ b/spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/configuration/TaskJobLauncherAutoConfigurationTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2018 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.cloud.task.batch.configuration; + +import org.junit.Test; + +import org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration; +import org.springframework.boot.autoconfigure.batch.JobLauncherCommandLineRunner; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.task.batch.handler.TaskJobLauncherCommandLineRunner; +import org.springframework.cloud.task.batch.listener.TaskBatchExecutionListenerTests; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Glenn Renfro + */ +public class TaskJobLauncherAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(). + withUserConfiguration(TaskBatchExecutionListenerTests.JobConfiguration.class, + PropertyPlaceholderAutoConfiguration.class, + EmbeddedDataSourceConfiguration.class, + BatchAutoConfiguration.class, + TaskJobLauncherAutoConfiguration.class); + + @Test + public void testAutoBuiltDataSourceWithTaskJobLauncherCLR() { + this.contextRunner. + withPropertyValues("spring.cloud.task.batch.commandLineRunnerEnabled=true"). + run(context -> { + assertThat(context).hasSingleBean(TaskJobLauncherCommandLineRunner.class); + assertThat(context).doesNotHaveBean(JobLauncherCommandLineRunner.class); + }); + } + + @Test + public void testAutoBuiltDataSourceWithTaskJobLauncherCLRDisabled() { + this.contextRunner.run(context -> { + assertThat(context).hasSingleBean(JobLauncherCommandLineRunner.class); + assertThat(context).doesNotHaveBean(TaskJobLauncherCommandLineRunner.class); + }); + } +} diff --git a/spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/handler/TaskJobLauncherCommandLineRunnerCoreTests.java b/spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/handler/TaskJobLauncherCommandLineRunnerCoreTests.java new file mode 100644 index 00000000..bbb04417 --- /dev/null +++ b/spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/handler/TaskJobLauncherCommandLineRunnerCoreTests.java @@ -0,0 +1,210 @@ +/* + * Copyright 2018 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.cloud.task.batch.handler; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.configuration.annotation.BatchConfigurer; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; +import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; +import org.springframework.batch.core.explore.JobExplorer; +import org.springframework.batch.core.explore.support.MapJobExplorerFactoryBean; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.launch.support.RunIdIncrementer; +import org.springframework.batch.core.launch.support.SimpleJobLauncher; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.support.transaction.ResourcelessTransactionManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.task.listener.TaskException; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.SyncTaskExecutor; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.transaction.PlatformTransactionManager; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Glenn Renfro + */ +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = {TaskJobLauncherCommandLineRunnerCoreTests.BatchConfiguration.class}) +public class TaskJobLauncherCommandLineRunnerCoreTests { + + @Autowired + private JobRepository jobRepository; + + @Autowired + private JobLauncher jobLauncher; + + @Autowired + private JobExplorer jobExplorer; + + @Autowired + PlatformTransactionManager transactionManager; + + private TaskJobLauncherCommandLineRunner runner; + + private JobBuilderFactory jobs; + + private StepBuilderFactory steps; + + private Job job; + + private Step step; + + @Before + public void init() { + this.jobs = new JobBuilderFactory(this.jobRepository); + this.steps = new StepBuilderFactory(this.jobRepository, this.transactionManager); + Tasklet tasklet = (contribution, chunkContext) -> null; + this.step = this.steps.get("step").tasklet(tasklet).build(); + this.job = this.jobs.get("job").start(this.step).build(); + this.runner = new TaskJobLauncherCommandLineRunner(this.jobLauncher, this.jobExplorer); + } + + + @DirtiesContext + @Test + public void basicExecution() throws Exception { + this.runner.execute(this.job, new JobParameters()); + assertThat(this.jobExplorer.getJobInstances("job", 0, 100)).hasSize(1); + this.runner.execute(this.job, + new JobParametersBuilder().addLong("id", 1L).toJobParameters()); + assertThat(this.jobExplorer.getJobInstances("job", 0, 100)).hasSize(2); + } + + @DirtiesContext + @Test + public void incrementExistingExecution() throws Exception { + this.job = this.jobs.get("job").start(this.step) + .incrementer(new RunIdIncrementer()).build(); + this.runner.execute(this.job, new JobParameters()); + this.runner.execute(this.job, new JobParameters()); + assertThat(this.jobExplorer.getJobInstances("job", 0, 100)).hasSize(2); + } + + @DirtiesContext + @Test + public void retryFailedExecution() throws Exception { + this.job = this.jobs.get("job") + .start(this.steps.get("step").tasklet(throwingTasklet()).build()) + .incrementer(new RunIdIncrementer()).build(); + runFailedJob(new JobParameters()); + runFailedJob(new JobParameters()); + assertThat(this.jobExplorer.getJobInstances("job", 0, 100)).hasSize(1); + } + + @DirtiesContext + @Test + public void retryFailedExecutionOnNonRestartableJob() throws Exception { + this.job = this.jobs.get("job").preventRestart() + .start(this.steps.get("step").tasklet(throwingTasklet()).build()) + .incrementer(new RunIdIncrementer()).build(); + runFailedJob(new JobParameters()); + runFailedJob(new JobParameters()); + // A failed job that is not restartable does not re-use the job params of + // the last execution, but creates a new job instance when running it again. + assertThat(this.jobExplorer.getJobInstances("job", 0, 100)).hasSize(2); + } + + @DirtiesContext + @Test + public void retryFailedExecutionWithNonIdentifyingParameters() throws Exception { + this.job = this.jobs.get("job") + .start(this.steps.get("step").tasklet(throwingTasklet()).build()) + .incrementer(new RunIdIncrementer()).build(); + JobParameters jobParameters = new JobParametersBuilder().addLong("id", 1L, false) + .addLong("foo", 2L, false).toJobParameters(); + runFailedJob(new JobParameters()); + runFailedJob(new JobParameters()); + assertThat(this.jobExplorer.getJobInstances("job", 0, 100)).hasSize(1); + } + + private Tasklet throwingTasklet() { + return (contribution, chunkContext) -> { + throw new RuntimeException("Planned"); + }; + } + + private void runFailedJob(JobParameters jobParameters) throws Exception { + boolean isExceptionThrown = false; + try { + this.runner.execute(this.job, new JobParameters()); + } + catch (TaskException taskException) { + isExceptionThrown = true; + } + assertThat(isExceptionThrown).isTrue(); + } + + @Configuration + @EnableBatchProcessing + protected static class BatchConfiguration implements BatchConfigurer { + + private ResourcelessTransactionManager transactionManager = + new ResourcelessTransactionManager(); + + private JobRepository jobRepository; + + private MapJobRepositoryFactoryBean jobRepositoryFactory = + new MapJobRepositoryFactoryBean( + this.transactionManager); + + public BatchConfiguration() throws Exception { + this.jobRepository = this.jobRepositoryFactory.getObject(); + } + + public void clear() { + this.jobRepositoryFactory.clear(); + } + + @Override + public JobRepository getJobRepository() { + return this.jobRepository; + } + + @Override + public PlatformTransactionManager getTransactionManager() { + return this.transactionManager; + } + + @Override + public JobLauncher getJobLauncher() { + SimpleJobLauncher launcher = new SimpleJobLauncher(); + launcher.setJobRepository(this.jobRepository); + launcher.setTaskExecutor(new SyncTaskExecutor()); + return launcher; + } + + @Override + public JobExplorer getJobExplorer() throws Exception { + return new MapJobExplorerFactoryBean(this.jobRepositoryFactory).getObject(); + } + + } +} diff --git a/spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/handler/TaskJobLauncherCommandLineRunnerTests.java b/spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/handler/TaskJobLauncherCommandLineRunnerTests.java new file mode 100644 index 00000000..90353184 --- /dev/null +++ b/spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/handler/TaskJobLauncherCommandLineRunnerTests.java @@ -0,0 +1,213 @@ +/* + * Copyright 2018 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.cloud.task.batch.handler; + +import java.util.Set; + +import org.assertj.core.api.Assertions; +import org.junit.After; +import org.junit.Test; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; +import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration; +import org.springframework.boot.autoconfigure.batch.JobLauncherCommandLineRunner; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.task.batch.configuration.TaskBatchAutoConfiguration; +import org.springframework.cloud.task.batch.configuration.TaskJobLauncherAutoConfiguration; +import org.springframework.cloud.task.configuration.EnableTask; +import org.springframework.cloud.task.repository.TaskExecution; +import org.springframework.cloud.task.repository.TaskExplorer; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +/** + * @author Glenn Renfro + */ +public class TaskJobLauncherCommandLineRunnerTests { + + private ConfigurableApplicationContext applicationContext; + + @After + public void tearDown() { + if (this.applicationContext != null) { + this.applicationContext.close(); + } + } + + @Test + public void testTaskJobLauncherCLRSuccessFail() { + String[] enabledArgs = new String[] { "--spring.cloud.task.batch.commandLineRunnerEnabled=true" }; + boolean isExceptionThrown = false; + try { + this.applicationContext = SpringApplication + .run(new Class[] { TaskJobLauncherCommandLineRunnerTests.JobWithFailureConfiguration.class, + PropertyPlaceholderAutoConfiguration.class, + EmbeddedDataSourceConfiguration.class, + BatchAutoConfiguration.class, + TaskBatchAutoConfiguration.class, + TaskJobLauncherAutoConfiguration.class }, enabledArgs); + } + catch (IllegalStateException exception) { + isExceptionThrown = true; + } + assertThat(isExceptionThrown).isTrue(); + } + + @Test + public void testTaskJobLauncherPickOneJob() { + String[] enabledArgs = new String[] { + "--spring.cloud.task.batch.commandLineRunnerEnabled=true", + "--spring.cloud.task.batch.jobNames=jobSucceed" }; + boolean isExceptionThrown = false; + try { + this.applicationContext = SpringApplication + .run(new Class[] { TaskJobLauncherCommandLineRunnerTests.JobWithFailureConfiguration.class, + PropertyPlaceholderAutoConfiguration.class, + EmbeddedDataSourceConfiguration.class, + BatchAutoConfiguration.class, + TaskBatchAutoConfiguration.class, + TaskJobLauncherAutoConfiguration.class }, enabledArgs); + } + catch (IllegalStateException exception) { + isExceptionThrown = true; + } + assertThat(isExceptionThrown).isFalse(); + validateContext(); + } + + @Test + public void testCommandLineRunnerSetToFalse() { + String[] enabledArgs = new String[] { }; + this.applicationContext = SpringApplication + .run(new Class[] { TaskJobLauncherCommandLineRunnerTests.JobConfiguration.class, + PropertyPlaceholderAutoConfiguration.class, + EmbeddedDataSourceConfiguration.class, + BatchAutoConfiguration.class, + TaskBatchAutoConfiguration.class, + TaskJobLauncherAutoConfiguration.class }, enabledArgs); + validateContext(); + assertThat(applicationContext.getBean(JobLauncherCommandLineRunner.class)).isNotNull(); + boolean exceptionThrown = false; + try { + applicationContext.getBean(TaskJobLauncherCommandLineRunner.class); + } + catch (NoSuchBeanDefinitionException exception) { + exceptionThrown = true; + } + assertThat(exceptionThrown).isTrue(); + validateContext(); + } + + private void validateContext() { + TaskExplorer taskExplorer = this.applicationContext.getBean(TaskExplorer.class); + + Page page = taskExplorer.findTaskExecutionsByName("application", PageRequest.of(0, 1)); + + Set jobExecutionIds = taskExplorer + .getJobExecutionIdsByTaskExecutionId(page.iterator().next().getExecutionId()); + + assertThat(jobExecutionIds.size()).isEqualTo(1); + assertThat(taskExplorer.getTaskExecution(jobExecutionIds.iterator().next()).getExecutionId()).isEqualTo(1); + + } + + @Configuration + @EnableBatchProcessing + @EnableTask + public static class JobConfiguration { + + @Autowired + private JobBuilderFactory jobBuilderFactory; + + @Autowired + private StepBuilderFactory stepBuilderFactory; + + @Bean + public Job job() { + return jobBuilderFactory.get("job") + .start(stepBuilderFactory.get("step1").tasklet(new Tasklet() { + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) + throws Exception { + System.out.println("Executed"); + return RepeatStatus.FINISHED; + } + }).build()) + .build(); + } + } + + @Configuration + @EnableBatchProcessing + @EnableTask + public static class JobWithFailureConfiguration { + + @Autowired + private JobBuilderFactory jobBuilderFactory; + + @Autowired + private StepBuilderFactory stepBuilderFactory; + + @Bean + public Job jobFail() { + return jobBuilderFactory.get("jobA") + .start(stepBuilderFactory.get("step1").tasklet(new Tasklet() { + @Override + public RepeatStatus execute(StepContribution contribution, + ChunkContext chunkContext) + throws Exception { + System.out.println("Executed"); + throw new IllegalStateException("WHOOPS"); + } + }).build()) + .build(); + } + + @Bean + public Job jobFun() { + return jobBuilderFactory.get("jobSucceed") + .start(stepBuilderFactory.get("step1Succeed").tasklet(new Tasklet() { + @Override + public RepeatStatus execute(StepContribution contribution, + ChunkContext chunkContext) + throws Exception { + System.out.println("Executed"); + return RepeatStatus.FINISHED; + } + }).build()) + .build(); + } + } +} diff --git a/spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/listener/TaskBatchExecutionListenerTests.java b/spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/listener/TaskBatchExecutionListenerTests.java index 52dcf98d..6c613cd4 100644 --- a/spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/listener/TaskBatchExecutionListenerTests.java +++ b/spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/listener/TaskBatchExecutionListenerTests.java @@ -65,7 +65,7 @@ import static org.junit.Assert.assertEquals; */ public class TaskBatchExecutionListenerTests { - public static final String[] ARGS = new String[] {"--spring.cloud.task.closecontext_enable=false"}; + public static final String[] ARGS = new String[] {}; private ConfigurableApplicationContext applicationContext; diff --git a/spring-cloud-task-docs/src/main/asciidoc/batch.adoc b/spring-cloud-task-docs/src/main/asciidoc/batch.adoc index 5be59741..9b58f9be 100644 --- a/spring-cloud-task-docs/src/main/asciidoc/batch.adoc +++ b/spring-cloud-task-docs/src/main/asciidoc/batch.adoc @@ -243,8 +243,15 @@ is covered in detail in the section As discussed before Spring Cloud Task applications support the ability to record the exit code of a task execution. However in cases where a user is running a Spring Batch Job within a task, regardless of how the Batch Job -Execution completes the result of the task will always be zero when using default Batch/Boot behavior. Keep in mind -that a task is a boot application and the exit code returned from the task is the same as a boot application. So -to have your task return the exit code based on the result of the batch job execution, you will need to write your -own CommandLineRunner. +Execution completes the result of the task will always be zero when using default Batch/Boot behavior. +Keep in mind that a task is a boot application and the exit code returned from +the task is the same as a boot application. +To override this behavior and allow the task to return an exit code other than zero +(either 1 `the default` or based on the +https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-spring-application.html#boot-features-application-exit[ExitCodeGenerator] +specified) upon a batch job returning an +https://docs.spring.io/spring-batch/4.0.x/reference/html/step.html#conditionalFlow[ExitStatus] +of "FAILED", set `spring.cloud.task.batch.commandLineRunnerEnabled` to true. +To have your task return the exit code based on the result of the batch job +execution, you will need to write your own `CommandLineRunner`.