Application that lets users test task/batch scenarios with one app

resolves #140
This commit is contained in:
Glenn Renfro
2020-11-19 17:07:56 -05:00
committed by Janne Valkealahti
parent 0d0c9b9d73
commit 5cbd231125
15 changed files with 1409 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
package io.spring.scenariotask;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ScenarioTaskApplication {
public static void main(String[] args) {
SpringApplication.run(ScenarioTaskApplication.class, args);
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2020 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 io.spring.scenariotask.configuration;
/**
* Exception thrown when user requests an error to occur during Task or Batch executions.
*
* @author Glenn Renfro
*/
public class ExpectedException extends Exception{
public ExpectedException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2020 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 io.spring.scenariotask.configuration;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Properties that allow the user to specify how they want the Batch/Task app to behave for a test.
*
* @author Glenn Renfro
*/
@ConfigurationProperties(prefix = "io.spring")
public class ScenarioProperties {
/**
* The name associated with the batch job. The default is "scenario-job".
*/
private String jobName = "scenario-job";
/**
* The name associated with the single step for the job. The default is "scenario-step".
*/
private String stepName = "scenario-step";
/**
* If true, the batch will throw a {@link ExpectedException}. Defaults to false.
*/
private boolean failBatch;
/**
* If true, the task will throw a {@link ExpectedException}. Defaults to false.
*/
private boolean failTask;
/**
* If true, the task will launch a sample batch job. Defaults to true.
*/
private boolean launchBatchJob = true;
/**
* How long the batch job should pause in the step. Defaults to 0.
*/
private int pauseInSeconds = 0;
/**
* If true a runIdIncrementer will be applied to the batch job. Defaults to false.
*/
private boolean includeRunidIncrementer;
public String getJobName() {
return jobName;
}
public void setJobName(String jobName) {
this.jobName = jobName;
}
public boolean isFailBatch() {
return failBatch;
}
public void setFailBatch(boolean failBatch) {
this.failBatch = failBatch;
}
public boolean isFailTask() {
return failTask;
}
public void setFailTask(boolean failTask) {
this.failTask = failTask;
}
public int getPauseInSeconds() {
return pauseInSeconds;
}
public void setPauseInSeconds(int pauseInSeconds) {
this.pauseInSeconds = pauseInSeconds;
}
public String getStepName() {
return stepName;
}
public void setStepName(String stepName) {
this.stepName = stepName;
}
public boolean isLaunchBatchJob() {
return launchBatchJob;
}
public void setLaunchBatchJob(boolean launchBatchJob) {
this.launchBatchJob = launchBatchJob;
}
public boolean isIncludeRunidIncrementer() {
return includeRunidIncrementer;
}
public void setIncludeRunidIncrementer(boolean includeRunidIncrementer) {
this.includeRunidIncrementer = includeRunidIncrementer;
}
}

View File

@@ -0,0 +1,116 @@
/*
* Copyright 2020 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 io.spring.scenariotask.configuration;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobInstance;
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.job.builder.SimpleJobBuilder;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.task.configuration.EnableTask;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Configure the Task and or Batch components of the test application.
*
* @author Glenn Renfro
*/
@EnableTask
@EnableBatchProcessing
@Configuration
@EnableConfigurationProperties(ScenarioProperties.class)
public class ScenarioTaskConfiguration {
private static final Log logger = LogFactory.getLog(ScenarioTaskConfiguration.class);
@Autowired
public JobBuilderFactory jobBuilderFactory;
@Autowired
public StepBuilderFactory stepBuilderFactory;
@Autowired
public JobExplorer jobExplorer;
@Autowired
public ScenarioProperties properties;
@Bean
@ConditionalOnProperty(
value = "io.spring.launchBatchJob",
havingValue = "true",
matchIfMissing = true)
public Job pausedemoAgain() {
SimpleJobBuilder jobBuilder = this.jobBuilderFactory.get(properties.getJobName())
.start(this.stepBuilderFactory.get(properties.getStepName())
.tasklet((contribution, chunkContext) -> {
logger.info(String.format("%s is starting", properties.getStepName()));
if (properties.getPauseInSeconds() > 0) {
logger.info(String.format("%s is pausing", properties.getStepName()));
Thread.sleep(properties.getPauseInSeconds() * 1000);
}
logger.info(String.format("%s is completing", properties.getStepName()));
if (jobExecutionCount() == 1 && properties.isFailBatch()) {
throw new ExpectedException("Exception thrown during Batch Execution");
}
return RepeatStatus.FINISHED;
})
.build());
if (this.properties.isIncludeRunidIncrementer()) {
jobBuilder.incrementer(new RunIdIncrementer());
}
return jobBuilder.build();
}
private int jobExecutionCount() {
JobInstance jobInstance = jobExplorer.getLastJobInstance(this.properties.getJobName());
List<JobExecution> jobExecutions = jobExplorer.getJobExecutions(jobInstance);
return jobExecutions.size();
}
/**
* Displays simple log message. If user specifies {@code io.spring.fail-task=true} a {@link ExpectedException} is thrown.
*
* @return ApplicationRunner instance for the app.
*/
@Bean
public ApplicationRunner applicationRunner(ScenarioProperties properties) {
return args -> {
logger.info("ApplicationRunner Executing for ScenarioTaskApplication");
if (properties.isFailTask()) {
throw new ExpectedException("Exception thrown during Task Execution");
}
};
}
}

View File

@@ -0,0 +1 @@
configuration-properties.classes=io.spring.scenariotask.configuration.ScenarioProperties

View File

@@ -0,0 +1 @@
logging.level.org.springframework.cloud.task=debug

View File

@@ -0,0 +1,227 @@
/*
* Copyright 2020 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 io.spring.scenariotask;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import io.spring.scenariotask.configuration.ExpectedException;
import javax.sql.DataSource;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.explore.support.JobExplorerFactoryBean;
import org.springframework.batch.core.repository.dao.Jackson2ExecutionContextStringSerializer;
import org.springframework.boot.SpringApplication;
import org.springframework.cloud.task.listener.TaskException;
import org.springframework.cloud.task.repository.TaskExecution;
import org.springframework.cloud.task.repository.TaskExplorer;
import org.springframework.cloud.task.repository.support.SimpleTaskExplorer;
import org.springframework.cloud.task.repository.support.TaskExecutionDaoFactoryBean;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@Testcontainers
public class ScenarioTaskApplicationTests {
private static DataSource dataSource;
private static JobExplorer jobExplorer;
private static TaskExplorer taskExplorer;
@Container
private static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer("postgres:11.1")
.withDatabaseName("integration-tests-db")
.withUsername("sa")
.withPassword("sa");
@BeforeAll
public static void initializeDB() throws Exception {
DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
driverManagerDataSource.setDriverClassName("org.postgresql.Driver");
driverManagerDataSource.setUrl(postgreSQLContainer.getJdbcUrl());
driverManagerDataSource.setUsername(postgreSQLContainer.getUsername());
driverManagerDataSource.setPassword(postgreSQLContainer.getPassword());
dataSource = driverManagerDataSource;
jobExplorer = jobExplorer();
taskExplorer = taskExplorer();
}
private static JobExplorer jobExplorer() throws Exception {
JobExplorerFactoryBean factoryBean = new JobExplorerFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setSerializer(new Jackson2ExecutionContextStringSerializer());
factoryBean.setJdbcOperations(new JdbcTemplate(dataSource));
return factoryBean.getObject();
}
private static TaskExplorer taskExplorer() {
TaskExecutionDaoFactoryBean taskExecutionDaoFactoryBean = new TaskExecutionDaoFactoryBean(dataSource, "TASK_");
return new SimpleTaskExplorer(taskExecutionDaoFactoryBean);
}
@Test
void testSuccessfulTask() {
String taskName = "taskSuccessfulTask";
List<String> args = getTaskLaunchArgs(taskName);
SpringApplication.run(ScenarioTaskApplication.class,
args.toArray(new String[0]));
List<TaskExecution> taskExecutions = getTaskExecutions(taskName);
assertThat(taskExecutions.size()).isEqualTo(1);
assertThat(taskExecutions.get(0).getExitCode()).isEqualTo(0);
}
@Test
void testFailTask() {
String taskName = "testFailTask";
List<String> args = getTaskLaunchArgs(taskName);
args.add("--io.spring.fail-task=true");
launchAppVerifyTaskFails(args, ExpectedException.class);
List<TaskExecution> taskExecutions = getTaskExecutions(taskName);
assertThat(taskExecutions.size()).isEqualTo(1);
assertThat(taskExecutions.get(0).getExitCode()).isEqualTo(1);
}
@Test
void testSuccessTaskWithFailedBatchAndRestart() {
final String jobName = "testSuccessTaskWithFailedBatchAndRestart";
SpringApplication.run(ScenarioTaskApplication.class,
getFailBatchArgs(jobName).toArray(new String[0])
);
List<JobExecution> jobExecutions = getJobExecutionsForLastJobInstance(jobName);
assertThat(jobExecutions.size()).isEqualTo(1);
assertThat(jobExecutions.get(0).getExitStatus().getExitCode()).isEqualTo(ExitStatus.FAILED.getExitCode());
assertThat(jobExecutions.get(0).getExitStatus().getExitDescription()).startsWith("io.spring.scenariotask.configuration.ExpectedException");
SpringApplication.run(ScenarioTaskApplication.class,
getFailBatchArgs(jobName).toArray(new String[0]));
jobExecutions = getJobExecutionsForLastJobInstance(jobName);
assertThat(jobExecutions.size()).isEqualTo(2);
assertThat(jobExecutions.get(0).getExitStatus().getExitCode()).isEqualTo(ExitStatus.COMPLETED.getExitCode());
}
@Test
void testSuccessTaskSuccessBatchAndRestartFailure() throws Exception{
final String jobName = "testSuccessTaskSuccessBatchAndRestartFailure";
List<String> args = getSuccessBatchArgs(jobName);
args.add("--io.spring.include-runid-incrementer=true");
SpringApplication.run(ScenarioTaskApplication.class,
args.toArray(new String[0])
);
List<JobExecution> jobExecutions = getJobExecutionsForLastJobInstance(jobName);
assertThat(jobExecutions.size()).isEqualTo(1);
assertThat(jobExecutions.get(0).getExitStatus().getExitCode()).isEqualTo(ExitStatus.COMPLETED.getExitCode());
SpringApplication.run(ScenarioTaskApplication.class,
args.toArray(new String[0]));
assertThat(jobExplorer.getJobInstanceCount(jobName)).isEqualTo(2);
}
@Test
void testFailTaskWithFailedBatchAndRestart() {
final String jobName = "testFailTaskWithFailedBatchAndRestart";
final String taskName = "testFailTaskWithFailedBatchAndRestartTask";
List<String> args = getFailBatchArgs(jobName);
args.add("--spring.application.name=" + taskName);
args.add("--spring.cloud.task.batch.fail-on-job-failure=true");
launchAppVerifyTaskFails(args, TaskException.class);
List<JobExecution> jobExecutions = getJobExecutionsForLastJobInstance(jobName);
assertThat(jobExecutions.size()).isEqualTo(1);
assertThat(jobExecutions.get(0).getExitStatus().getExitCode()).isEqualTo(ExitStatus.FAILED.getExitCode());
assertThat(jobExecutions.get(0).getExitStatus().getExitDescription()).startsWith("io.spring.scenariotask.configuration.ExpectedException");
List<TaskExecution> taskExecutions = getTaskExecutions(taskName);
assertThat(taskExecutions.size()).isEqualTo(1);
assertThat(taskExecutions.get(0).getExitCode()).isEqualTo(1);
SpringApplication.run(ScenarioTaskApplication.class,
args.toArray(new String[0]));
jobExecutions = getJobExecutionsForLastJobInstance(jobName);
assertThat(jobExecutions.size()).isEqualTo(2);
assertThat(jobExecutions.get(0).getExitStatus().getExitCode()).isEqualTo(ExitStatus.COMPLETED.getExitCode());
}
@Test
void testSuccessfulTaskBatch() {
final String jobName = "testSuccessfulTaskBatch";
SpringApplication.run(ScenarioTaskApplication.class,
getSuccessBatchArgs(jobName).toArray(new String[0]));
List<JobExecution> jobExecutions = getJobExecutionsForLastJobInstance(jobName);
assertThat(jobExecutions.size()).isEqualTo(1);
assertThat(jobExecutions.get(0).getExitStatus().getExitCode()).isEqualTo(ExitStatus.COMPLETED.getExitCode());
}
private void launchAppVerifyTaskFails(List<String> args, Class clazz) {
assertThatThrownBy(() -> {
SpringApplication.run(ScenarioTaskApplication.class,
args.toArray(new String[0]));
}).isInstanceOf(IllegalStateException.class)
.getCause().isInstanceOf(clazz);
}
private List<JobExecution> getJobExecutionsForLastJobInstance(String jobName) {
long instanceId = jobExplorer.getLastJobInstance(jobName).getInstanceId();
return jobExplorer.getJobExecutions(jobExplorer.getJobInstance(instanceId));
}
private List<String> getTaskLaunchArgs(String taskName) {
List<String> args = new ArrayList<>(getDatabaseArgs());
args.add("--io.spring.launchBatchJob=false");
args.add("--spring.application.name=" + taskName);
return args;
}
private List<String> getDatabaseArgs() {
List<String> args = Arrays.asList(
"--spring.datasource.url=" + postgreSQLContainer.getJdbcUrl(),
"--spring.datasource.driverClassName=" + "org.postgresql.Driver",
"--spring.datasource.username=" + postgreSQLContainer.getUsername(),
"--spring.datasource.password=" + postgreSQLContainer.getPassword());
return args;
}
private List<String> getSuccessBatchArgs(String jobName) {
List<String> args = new ArrayList<>(getDatabaseArgs());
args.add("--io.spring.job-name=" + jobName);
args.add("--spring.batch.initialize-schema=always");
return args;
}
private List<String> getFailBatchArgs(String jobName) {
List<String> args = new ArrayList<>(getSuccessBatchArgs(jobName));
args.add("--io.spring.fail-batch=true");
return args;
}
private List<TaskExecution> getTaskExecutions(String taskName) {
Page<TaskExecution> taskExecutionPage = taskExplorer.findTaskExecutionsByName(taskName, PageRequest.of(0, 5));
return taskExecutionPage.getContent();
}
}