Files
spring-boot-demo/spring-batch/docs/08-skip-vs-restart.md
T
Claude b81af72bc3 Add spring-batch: jobs, steps, chunk processing and restartability on Boot 4.1
Companion code for "Spring Batch on Boot 4.1: Jobs, Steps, Chunk Processing and
Restartability". A productImportJob configured three ways by profile against a
poisoned CSV row, run as real java -jar processes (not just JUnit) so the
restart story is genuine: a chunk fails and rolls back, the process exits, a
brand-new JVM against the same file-based H2 database resumes at the exact
next unread row (READ_COUNT 20, not 60) and completes.

Findings the build pins:
- StepBuilder.chunk(int, PlatformTransactionManager) still compiles in Batch
  6.0.5 but returns the legacy SimpleStepBuilder; chunk(int) returns the new
  ChunkOrientedStepBuilder, and only the latter is used here.
- Two different ExecutionContext classes now exist in two different packages
  (infrastructure.item vs core.repository.persistence) with different shapes.
- spring-boot-starter-batch alone gives a resourceless JobRepository that
  forgets every JobInstance the moment the JVM exits; spring-boot-starter-
  batch-jdbc is what makes the restart demo possible at all, demonstrated by
  excluding BatchJdbcAutoConfiguration and watching a "restart" collide with
  the previous run's own data instead of resuming it.
- A migration-guide summary claiming CommandLineJobRunner was removed in 6.0
  is wrong -- javap against the real jar shows @Deprecated(forRemoval=true),
  not removed.
- RepeatStatus moved from core.repeat to infrastructure.repeat, caught by the
  compiler rather than by reading docs.

11 documentation chapters, 10 captured transcripts (unit tests, javap output,
and real two-JVM scenario runs), all regenerated by scripts/run-all.sh.

Fixed after push: three dead docs.spring.io links in the doc chapters
(readersAndWriters/* and chunk-oriented-processing/*.html paths moved when
Spring Batch 6 reorganized its reference docs; corrected to the current
readers-and-writers/*, processor.html and chunk-oriented-processing.html
paths, verified 200 via curl before committing).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019DXsJ1zpikbA1MQJN6RqFA
2026-09-13 06:37:46 +00:00

5.2 KiB

8. Skip vs. restart, and the two knobs that shape a restart

← Previous | README | Next: Corrections found while writing this →

Failing the job and restarting after a fix (chapter 7) is one answer to a bad row. It is the right one when the row is genuinely wrong and someone needs to decide what to do about it. It is the wrong one when the bad row is expected background noise — a handful of duplicates in a million-row feed — and stopping the whole job for each one is not realistic.

The skip profile configures the same step with faultTolerant().skip(...) instead:

new StepBuilder("importStep", jobRepository)
        .<Product, Product>chunk(10)
        .transactionManager(transactionManager)
        .reader(productReader)
        .processor(productProcessor)
        .writer(productWriter)
        .faultTolerant()
        .skip(DataIntegrityViolationException.class)
        .skipLimit(3)
        .listener(skipListener())
        .build();

Run against the same poisoned file used in chapter 7, in one pass, no restart needed:

$ SELECT status, read_count, filter_count, write_count, write_skip_count, commit_count, rollback_count FROM BATCH_STEP_EXECUTION;
COMPLETED | 60         | 2            | 57          | 1                | 6            | 2

$ SELECT COUNT(*) FROM PRODUCT;
COUNT(*)
57

Full transcript: docs/output/10-skip-instead-of-fail.txt. The job reads all 60 rows in one execution, filters the same 2 as always, skips exactly the duplicate (WRITE_SKIP_COUNT 1), and completes with 57 rows written. Note ROLLBACK_COUNT is 2, not 0: when a fault-tolerant chunk's batch write fails, Spring Batch does not give up on the chunk — it rolls back once, then re-processes that chunk's items one at a time to find out which single item is the actual problem, so it can skip only that one and keep the rest. That retry-by-scanning is one extra rollback for the chunk containing the bad row; you can see it in the count.

DataIntegrityViolationException.class catches DuplicateKeyException because of the hierarchy

The skip policy is registered against DataIntegrityViolationException, but the exception H2 actually throws is DuplicateKeyException. This works because DuplicateKeyException extends DataIntegrityViolationException — Spring's JDBC exception translation maps H2's JdbcBatchUpdateException (a unique-constraint violation) to the more specific subclass, and a skip policy registered against the parent still matches it. Confirmed directly, not assumed:

thrown type        : org.springframework.dao.DuplicateKeyException
is a DataIntegrityViolationException? true

(from docs/output/02-exception-hierarchy.txt, produced by ProductValidatingProcessorTest.duplicateKeyExceptionIsADataIntegrityViolationException). Registering skip policies against a broad parent type like DataIntegrityViolationException means you do not have to enumerate every specific subtype Spring's translation layer might produce — but it also means you are choosing to skip any data-integrity problem, not just duplicates. skipLimit(3) is the safety valve: past 3 skips in one execution, the step gives up and fails anyway, on the theory that "one bad row" and "the whole feed is corrupt" should not be handled identically.

The two restart-shaping options on a step

reportStep uses one of these; the football-job example in the Spring Batch reference documents both well, and this module's reportStep is a direct, smaller version of that pattern:

  • allowStartIfComplete(true) — by default, a step that already finished COMPLETED is skipped entirely on restart (the whole point of chapter 7 is not re-doing committed work). reportStep overrides that, because a status report should reflect the current state of the PRODUCT table on every run, including a restart, not just the first successful attempt.
  • startLimit(n) — not used in this module, but worth knowing: caps how many times a step may be attempted (not completed) before Spring Batch throws StartLimitExceededException instead of trying again. Useful for a step that does something non-idempotent enough that repeated automatic retries would make things worse — call out external systems, send notifications — where the right response to repeated failure is a human looking at it, not an unbounded retry loop.

Going deeper

Next: Corrections found while writing this →