spring-batch icon indicating copy to clipboard operation
spring-batch copied to clipboard

Terminates the job if a flag is set on afterStep

Open umbum opened this issue 2 years ago • 1 comments

As-is : Even if the terminateOnly flag is set on afterStep, It is ignored and the next step is executed. There is no way to terminate a job gracefully without using the on(ExitStatus) method when building the job.

To-be : If the terminateOnly flag is set, the job terminates gracefully.

It looks similar to https://github.com/spring-projects/spring-batch/issues/1213 but It's different. If developers want to explicitly terminate a job, they should be able to do so.

umbum avatar Sep 23 '23 04:09 umbum

Thank you for opening this PR. I am not sure "isTerminateOnly" was designed to be used in a step execution listener. My thinking is that in the afterStep, the step has already ended successfully or with a failure, so it is too late to set that flag at that point. The issue more is about stopping the surrounding job (as mentioned by the javadocs). And this is where things become tricky, which I admit can be confusing.

There is a key difference between a SimpleJob and a FlowJob: Unlike FlowJob, SimpleJob is designed only for sequential executions of steps until the last step succeeds or one of the steps fail. So this implementation was not designed to be stopped programmatically.

Here is a quick example:

import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.StepExecutionListener;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.support.JdbcTransactionManager;

import javax.sql.DataSource;

@Configuration
@EnableBatchProcessing
public class HelloWorldJobConfiguration {

	public static void main(String[] args) throws Exception {
		ApplicationContext context = new AnnotationConfigApplicationContext(HelloWorldJobConfiguration.class);
		JobLauncher jobLauncher = context.getBean(JobLauncher.class);
		Job job = context.getBean(Job.class);
		jobLauncher.run(job, new JobParameters());
	}

	@Bean
	public Step step1(JobRepository jobRepository, JdbcTransactionManager transactionManager) {
		return new StepBuilder("step1", jobRepository)
				.tasklet((contribution, chunkContext) -> {
			System.out.println("Hello");
			return RepeatStatus.FINISHED;
		}, transactionManager)
				.listener(new StepExecutionListener() {
					@Override
					public ExitStatus afterStep(StepExecution stepExecution) {
						stepExecution.setTerminateOnly();
						return stepExecution.getExitStatus();
					}
				})
				.build();
	}

	@Bean
	public Step step2(JobRepository jobRepository, JdbcTransactionManager transactionManager) {
		return new StepBuilder("step2", jobRepository).tasklet((contribution, chunkContext) -> {
			System.out.println("world!");
			return RepeatStatus.FINISHED;
		}, transactionManager).build();
	}

	@Bean
	public Job job(JobRepository jobRepository, Step step1, Step step2) {
		return new JobBuilder("job", jobRepository)
				.start(step1)
				.next(step2)
				.build();
	}

	// infra beans

	@Bean
	public DataSource dataSource() {
		return new EmbeddedDatabaseBuilder()
				.addScript("/org/springframework/batch/core/schema-hsqldb.sql")
				.addScript("/org/springframework/batch/samples/common/business-schema-hsqldb.sql")
				.generateUniqueName(true)
				.build();
	}

	@Bean
	public JdbcTransactionManager transactionManager(DataSource dataSource) {
		return new JdbcTransactionManager(dataSource);
	}

}

In this case, step2 runs even if step1 was flagged as terminateOnly. However, if I only change the job type to FlowJob, things behave as expected:

@Bean
public Job job(JobRepository jobRepository, Step step1, Step step2) {
   return new JobBuilder("job", jobRepository)
				.flow(step1)
				.next(step2)
				.build()
				.build();
}

Do you see the difference?

If developers want to explicitly terminate a job, they should be able to do so.

BTW, I find that stopping a job with a listener is implicit rather than explicit. The more explicit way would be to create a clear execution flow through the Java DSL or the XML namespace.

fmbenhassine avatar Feb 27 '25 14:02 fmbenhassine

Closing this as explained in my previous comment:

I am not sure "isTerminateOnly" was designed to be used in a step execution listener. My thinking is that in the afterStep, the step has already ended successfully or with a failure, so it is too late to set that flag at that point

But feel free to add a comment or open a PR if you think we can improve things in the documentation.

fmbenhassine avatar Nov 18 '25 15:11 fmbenhassine