# 7. Restartability: what actually resumes, and from where [← Previous](06-jdbc-writer-and-records.md) | [README](../README.md) | [Next: Skip vs. restart →](08-skip-vs-restart.md) This chapter is the "run 1 fails, run 2 in a brand-new JVM finishes the job" demonstration [the article](https://ankurm.com/) leads with. Two real, separate `java -jar` invocations, against the same file-based H2 database, produced everything below — see [`scripts/capture-scenarios.sh`](../scripts/capture-scenarios.sh) for the exact commands. ## Run 1: the poisoned duplicate fails a whole chunk `products-poison.csv` is identical to the clean fixture except row 47, where the SKU is changed to `ABC-0005` — a duplicate of row 5. Row 47 falls in chunk 5 (rows 41-50 at chunk size 10). The `broken` profile's step has no fault tolerance configured, so the `UNIQUE` constraint violation on that insert fails the batch statement, which fails the chunk, which fails the step, which fails the job: ```console $ SELECT status, read_count, filter_count, write_count, commit_count, rollback_count FROM BATCH_STEP_EXECUTION; STATUS | READ_COUNT | FILTER_COUNT | WRITE_COUNT | COMMIT_COUNT | ROLLBACK_COUNT FAILED | 50 | 2 | 38 | 4 | 1 ``` Full transcript: [`docs/output/07-restart-run1-fails.txt`](output/07-restart-run1-fails.txt). Four chunks (rows 1-40, minus the 2 filtered by the processor — see [chapter 4](04-item-processor-as-filter.md) — = 38 written) committed successfully before chunk 5 rolled back entirely. The reader had read through row 50 by the time the writer failed (`READ_COUNT` 50), but none of chunk 5's rows are in the `PRODUCT` table — the transaction that would have written them rolled back along with everything else in that chunk. ## The fix, and the restart Someone fixes the source data — here, changing row 47's SKU to something that is not a duplicate, in place, same line, same file: ```console sed -i '48s/ABC-0005/ABC-0999/' scenario-data/restart-demo/input.csv ``` Then the exact same command that produced run 1 runs again, in a completely new JVM process, using the exact same identifying job parameter (`batch.run=demo`, see [chapter 5](05-launching-and-jobparameters.md)): ```console $ SELECT step_execution_id, job_execution_id, status, read_count, write_count, commit_count, rollback_count FROM BATCH_STEP_EXECUTION ORDER BY step_execution_id; STEP_EXECUTION_ID | JOB_EXECUTION_ID | STATUS | READ_COUNT | WRITE_COUNT | COMMIT_COUNT | ROLLBACK_COUNT 1 | 1 | FAILED | 50 | 38 | 4 | 1 2 | 2 | COMPLETED | 20 | 20 | 2 | 0 $ SELECT COUNT(*) FROM PRODUCT; COUNT(*) 58 ``` Full transcript: [`docs/output/08-restart-run2-resumes.txt`](output/08-restart-run2-resumes.txt). Read that second row carefully: **`READ_COUNT` is 20, not 60.** The new execution did not start the CSV from row 1. It read only rows 41 through 60 — the two chunks that had not committed yet — wrote all 20 of them (the earlier two filtered rows were both in the first 40, so nothing left to filter here), and the job status flips to `COMPLETED`. `PRODUCT` ends up with 58 rows: the 38 that survived run 1, plus these 20.
Run 1 (JobExecution 1) 1-10 11-20 21-30 31-40 41-50 FAIL never read committed: rows 1-40 (38 written after 2 filtered) -- job FAILS at chunk 5, exit status FAILED Run 2 (JobExecution 2) -- fresh JVM, same database, corrected row 47 rows 1-40: already committed, NOT re-read 41-50 OK 51-60 OK READ_COUNT for this execution = 20, not 60. COMPLETED. PRODUCT now has 58 rows total.
## Where the resume position actually lives The reader's position is not something Spring Batch infers after the fact — it is saved, by the reader itself, into the step's `ExecutionContext` at every chunk commit, in the **same transaction** as the chunk's writes. `FlatFileItemReader` implements `ItemStream` (`org.springframework.batch.infrastructure.item.ItemStream`, see [`docs/output/04-two-executioncontext-classes.txt`](output/04-two-executioncontext-classes.txt) for its shape) and calls `update(ExecutionContext)` on every commit, storing the current line count. Because that update happens inside the chunk's transaction: - If the chunk commits, the new line count is persisted right alongside the rows it wrote. - If the chunk rolls back (chunk 5, run 1), the line-count update rolls back too — the persisted position stays at the end of chunk 4, not partway through chunk 5. That is the entire mechanism. There is no separate "resume point" concept to configure: it falls out of transactional chunk commits plus a reader that participates in `ItemStream`. Writing a custom reader that does not save its position to the `ExecutionContext` — a plain `ItemReader` with no `ItemStream` — means restarts always start that reader from scratch, which is sometimes exactly what you want (an idempotent reader) and sometimes a bug you will not notice until the first real failure in production. ## Going deeper - `ItemStream` and the two-argument `open`/`update` contract: [Spring Batch reference — readers and ItemStream](https://docs.spring.io/spring-batch/reference/readers-and-writers/item-reader.html) (`rel="nofollow"`). - Configuring restart limits per step (`startLimit`, `allowStartIfComplete`): [chapter 8](08-skip-vs-restart.md), and the [Spring Batch reference on restart configuration](https://docs.spring.io/spring-batch/reference/step/chunk-oriented-processing/restart.html) (`rel="nofollow"`). - What happens if the job repository itself does not persist across the JVM restart shown above: [chapter 10](10-resourceless-vs-jdbc.md) — this entire chapter depends on it being real. [Next: Skip vs. restart →](08-skip-vs-restart.md)