Skip to content

ChunkOrientedStepBuilder throws IllegalArgumentException when skip() is used(configured) without skipLimit() #5069

@KILL9-NO-MERCY

Description

@KILL9-NO-MERCY

Hello Spring Batch team

Bug description
When using ChunkOrientedStepBuilder.skip() to specify skippable exceptions without calling skipLimit(), the step builder fails with an IllegalArgumentException during the build phase. This occurs because skipLimit defaults to -1, which is rejected by LimitCheckingExceptionHierarchySkipPolicy constructor.

This is the same root cause as the retry configuration issue (#5068), where the default value is -1 and the validation logic rejects it.

Environment

  • Spring Batch version: 6.0.0-RC1
  • Spring Framework version: 7.0.0-RC2

Steps to reproduce

  1. Create a chunk-oriented step using StepBuilder
  2. Call .skip(SomeException.class) without calling .skipLimit()
  3. Attempt to build the step

Expected behavior
The step should build successfully without throwing an exception.

Actual behavior
Exception thrown:

Caused by: java.lang.IllegalArgumentException: The skipLimit must be greater than zero
	at org.springframework.util.Assert.isTrue(Assert.java:117)
	at org.springframework.batch.core.step.skip.LimitCheckingExceptionHierarchySkipPolicy.<init>(LimitCheckingExceptionHierarchySkipPolicy.java:45)
	at org.springframework.batch.core.step.builder.ChunkOrientedStepBuilder.build(ChunkOrientedStepBuilder.java:415)

Minimal Complete Reproducible example

@Configuration
public class IssueReproductionJobConfiguration {
    
    @Bean
    public Job issueReproductionJob(JobRepository jobRepository, Step issueReproductionStep) {
        return new JobBuilder(jobRepository)
                .start(issueReproductionStep)
                .build();
    }

    @Bean
    public Step issueReproductionStep(JobRepository jobRepository) {
        AtomicInteger counter = new AtomicInteger(0);

        return new StepBuilder(jobRepository)
                .chunk(5)
                .reader(() -> {
                    int count = counter.incrementAndGet();
                    if (count <= 5) {
                        return "kill-" + count;
                    }
                    return null;
                })
                .writer(items -> items.forEach(item ->
                        System.out.println("💀 Terminated: " + item)
                ))
                .faultTolerant()
                .skip(IOException.class)
                //.skipLimit(1)  // ← This must be added for proper operation
                .build();  // ← IllegalArgumentException thrown here
    }
}

Root cause analysis

In ChunkOrientedStepBuilder:

private long skipLimit = -1;  // Default value

public ChunkOrientedStep build() {
    // ...
    if (this.skipPolicy == null) {
        // This condition uses OR, so it's true when only skippableExceptions is set
        if (!this.skippableExceptions.isEmpty() || this.skipLimit > 0) {
            this.skipPolicy = new LimitCheckingExceptionHierarchySkipPolicy(
                this.skippableExceptions,
                this.skipLimit  // ← Passes -1 here
            );  // ← Fails here
        }
        else {
            this.skipPolicy = new AlwaysSkipItemSkipPolicy();
        }
    }
    // ...
}

In LimitCheckingExceptionHierarchySkipPolicy:

public LimitCheckingExceptionHierarchySkipPolicy(
        Set<Class> skippableExceptions,
        long skipLimit) {
    Assert.notEmpty(skippableExceptions, "The skippableExceptions must not be empty");
    Assert.isTrue(skipLimit > 0, "The skipLimit must be greater than zero");  // ← Rejects -1
    this.skippableExceptions = skippableExceptions;
    this.skipLimit = skipLimit;
}

Suggested fixes

Option 1: Change default value

private long skipLimit = 0;  // Change from -1 to 0

Option 2: Add documentation

Add JavaDoc to skip() method stating that skipLimit() must be called with a positive value (greater than 0).

/**
 * Configure exceptions that should be skipped.
 * Note: {@link #skipLimit(long)} must be called with a positive value 
 * before building the step when using this method.
 * @param skippableExceptions exceptions to skip
 * @return this for fluent chaining
 */
@SafeVarargs
public final ChunkOrientedStepBuilder skip(Class... skippableExceptions)

Thank you for reviewing this issue. Please let me know if you need any additional information or clarification.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions