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
93 lines
5.2 KiB
Markdown
93 lines
5.2 KiB
Markdown
# 8. Skip vs. restart, and the two knobs that shape a restart
|
|
|
|
[← Previous](07-restartability.md) | [README](../README.md) | [Next: Corrections found while writing this →](09-corrections.md)
|
|
|
|
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:
|
|
|
|
```java
|
|
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:
|
|
|
|
```console
|
|
$ 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`](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:
|
|
|
|
```console
|
|
thrown type : org.springframework.dao.DuplicateKeyException
|
|
is a DataIntegrityViolationException? true
|
|
```
|
|
|
|
(from [`docs/output/02-exception-hierarchy.txt`](output/02-exception-hierarchy.txt), produced by
|
|
[`ProductValidatingProcessorTest.duplicateKeyExceptionIsADataIntegrityViolationException`](../src/test/java/com/ankurm/batch/ProductValidatingProcessorTest.java)).
|
|
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
|
|
|
|
- Skip and retry configuration in full, including `noSkip()` exclusions and combining skip with
|
|
retry: [Spring Batch reference — configuring skip logic](https://docs.spring.io/spring-batch/reference/step/chunk-oriented-processing/configuring-skip.html) (`rel="nofollow"`).
|
|
- `startLimit` and `allowStartIfComplete` with the football-job example this chapter borrows the
|
|
shape of: [Spring Batch reference — configuring a step for restart](https://docs.spring.io/spring-batch/reference/step/chunk-oriented-processing/restart.html) (`rel="nofollow"`).
|
|
|
|
[Next: Corrections found while writing this →](09-corrections.md)
|