spring-projects / spring-projects/spring-batch

Process Does Not Terminate When Processing Multiple Items

Open
#4,515 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

status: waiting-for-reporter type: bug
Dominant language
Java
Stars
3k
Forks
2.5k
Avg merge
6d 53m
Merged PRs (30d)
3

Description

Bug description
I recently upgraded my project's SpringBoot version to 3, along with an upgrade to Spring Batch version 5.

However, I've encountered an issue while executing Batch processes. During the execution of ItemWriter logic, the process unexpectedly halts. Although I've enabled the show-sql option and observed that some queries are being generated, the intended queries do not fully execute and the process remains running.

The point at which it halts varies randomly with each execution, and very occasionally, all intended queries are executed, allowing the process to commit and terminate normally.

TL;DR:

Upgraded to SpringBoot 3.
When ItemReader processes a single item, the commit always executes, and the process terminates normally.
When processing more than one item, the process randomly stops at certain points, and neither commit nor process termination occurs.

Environment
Spring Batch 5.0.3 / Spring Boot 3.1.5 / Java 17 / JPA Hibernate 6

Steps to reproduce
Below is the code I modified while upgrading to version 5 of Spring Batch

  1. Remove @EnableBatchProcessing
  2. Add a Config class that implements DefaultBatchConfiguration.
@Configuration
public class BatchDBConfiguration extends DefaultBatchConfiguration {

    @BatchDataSource
    @Bean(name = "jobDataSource")
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource jobDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Override
    protected DataSource getDataSource() {
        return this.jobDataSource();
    }

    @Override
    protected PlatformTransactionManager getTransactionManager() {
        return this.jdbcTransactionManager();
    }

    @Bean(name = "jdbcTransactionManager")
    public PlatformTransactionManager jdbcTransactionManager() {
        return new JdbcTransactionManager(getDataSource());
    }

    @Override
    protected ExecutionContextSerializer getExecutionContextSerializer() {
        return new Jackson2ExecutionContextStringSerializer();
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(prefix = "spring.batch.job", name = "enabled", havingValue = "true", matchIfMissing = true)
    public JobLauncherApplicationRunner jobLauncherApplicationRunner(JobLauncher jobLauncher, JobExplorer jobExplorer,
                                                                     JobRepository jobRepository, BatchProperties properties) {
        JobLauncherApplicationRunner runner = new JobLauncherApplicationRunner(jobLauncher, jobExplorer, jobRepository);
        String jobNames = properties.getJob().getName();

        if (StringUtils.hasText(jobNames)) {
            runner.setJobName(jobNames);
        }
        return runner;
    }
}

Below is an example of one of the jobs where the issue is occurring.

@Configuration
public class ConvertToDormantUserBatchConfig {

    private static final String NAME = "convertToDormantUser";
    private static final String JOB = NAME + "Job";
    private static final String JOB_STEP = JOB + "Step";
    private static final int CHUNK_SIZE = 50;

    private final JobRepository jobRepository;
    private final PlatformTransactionManager platformTransactionManager;

    private final FindPreDormantUsers findPreDormantUsers;
    private final ConvertToDormantUser convertToDormantUser;

    private final ExecutionLoggingListener executionLoggingListener;
    private final TargetDateJobParameter convertToDormantUserJobParameter;
    private final Logger logger = LoggerFactory.getLogger("USER_BATCH");

    public ConvertToDormantUserBatchConfig(JobRepository jobRepository,
                                           @Qualifier("jpaTransactionManager") PlatformTransactionManager platformTransactionManager,
                                           @Qualifier("convertToDormantUserJobParameter") TargetDateJobParameter convertToDormantUserJobParameter,
                                           FindPreDormantUsers findPreDormantUsers,
                                           ConvertToDormantUser convertToDormantUser,
                                           ExecutionLoggingListener executionLoggingListener) {
        this.jobRepository = jobRepository;
        this.platformTransactionManager = platformTransactionManager;
        this.findPreDormantUsers = findPreDormantUsers;
        this.convertToDormantUser = convertToDormantUser;
        this.executionLoggingListener = executionLoggingListener;
        this.convertToDormantUserJobParameter = convertToDormantUserJobParameter;
    }

    @Bean(JOB + "Parameter")
    @JobScope
    public TargetDateJobParameter jobParameter(@Value("#{jobParameters[targetDate]}") String targetDate) {
        return new TargetDateJobParameter(targetDate);
    }

    @Bean
    public Job convertToDormantUserJob() {
        return new JobBuilder(JOB, jobRepository)
                .preventRestart()
                .incrementer(new UniqueRunIdIncrementer())
                .listener(executionLoggingListener)
                .start(convertToDormantUserJobStep())
                .build();
    }

    @Bean
    @JobScope
    public Step convertToDormantUserJobStep() {
        return new StepBuilder(JOB_STEP, jobRepository)
                .<FindPreDormantUsersResult, FindPreDormantUsersResult>chunk(CHUNK_SIZE, platformTransactionManager)
                .reader(convertToDormantUserReader())
                .writer(convertToDormantUserWriter())
                .listener(executionLoggingListener)
                .build();
    }

    @Bean
    @StepScope
    public ListItemReader<FindPreDormantUsersResult> convertToDormantUserReader() {
        var results = findPreDormantUsers.execute(
                new FindPreDormantUsersQuery(
                        CONVERT_DORMANT_USER_CRITERION,
                        convertToDormantUserJobParameter.getTargetDate()
                )
        );
        return new ListItemReader<>(results);
    }

    @Bean
    @StepScope
    public ItemWriter<FindPreDormantUsersResult> convertToDormantUserWriter() {
        return results -> {
            List<String> userTokens = results.getItems().stream().map(FindPreDormantUsersResult::getUserToken).collect(Collectors.toList());
            convertToDormantUser.execute(new ConvertToDormantUserCommand(userTokens));
        };
    }
}

I'm wondering if the TxManager is incorrectly configured. In the job, I'm injecting the jpaTransactionManager bean, which is the TxManager being used effectively throughout the application.

In the BatchConfig, I've created and registered a JdbcTransactionManager as a bean.

Expected behavior
I expect the process to commit successfully and terminate normally when processing multiple items


same question in stackOverFlow

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with BatchDBConfiguration and ConvertToDormantUserBatchConfig, then reproduce the job with multiple items using the Spring Batch 5.0.3, Spring Boot 3.1.5, and Java 17 environment described. Compare the configured transaction managers and verify that all items commit and the process terminates normally.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, spring, spring-boot
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.