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
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 finishedCOMPLETEDis skipped entirely on restart (the whole point of chapter 7 is not re-doing committed work).reportStepoverrides that, because a status report should reflect the current state of thePRODUCTtable 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 throwsStartLimitExceededExceptioninstead 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
- Skip and retry configuration in full, including
noSkip()exclusions and combining skip with retry: Spring Batch reference — configuring skip logic (rel="nofollow"). startLimitandallowStartIfCompletewith the football-job example this chapter borrows the shape of: Spring Batch reference — configuring a step for restart (rel="nofollow").