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

93 lines
5.2 KiB
Markdown

# 8. Skip vs. restart, and the two knobs that shape a restart
[&larr; Previous](07-restartability.md) | [README](../README.md) | [Next: Corrections found while writing this &rarr;](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 &mdash; a handful of duplicates in a
million-row feed &mdash; 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 &mdash; 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` &mdash; 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 &mdash; 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)`** &mdash; 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)`** &mdash; 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 &mdash; call out
external systems, send notifications &mdash; 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 &mdash; 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 &mdash; 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 &rarr;](09-corrections.md)