Files

65 lines
3.4 KiB
Markdown

# 5. The diagnostic endpoint: proving four threads ran, not just configuring them
[← Previous](04-the-writer-and-the-beanmapped-trap.md) | [README](../README.md) | [Next: Why CPU-bound, not I/O-bound →](06-why-cpu-bound-not-io-bound.md)
Configuration says how many partitions *should* run and on how many threads. It does not say what
actually happened on a given execution — whether the pool was saturated, whether two
partitions landed on the same thread, how long each one actually took relative to the others. This
module makes that observable instead of assumed.
## PartitionStatsListener
[`PartitionStatsListener`](../src/main/java/com/ankurm/batchpartition/partition/PartitionStatsListener.java)
is a `StepExecutionListener` attached to the worker step. `beforeStep` records a start timestamp on
a `ThreadLocal` (each partition runs on its own thread, so there is no cross-talk); `afterStep`
computes the duration and inserts one row into `PARTITION_STATS`: which step (which partition),
which OS thread name, how many rows it read, when it started and finished, how long it took, and
its exit code.
## PartitionInsightController
[`PartitionInsightController`](../src/main/java/com/ankurm/batchpartition/web/PartitionInsightController.java)
exposes that table over HTTP:
```console
$ curl -s http://localhost:8081/batch/partitions/1 | python3 -m json.tool
[
{
"PARTITION_NAME": "ordersWorkerStep:partition0",
"THREAD_NAME": "order-partition-2",
"READ_COUNT": 5000,
"STARTED_AT": "2026-09-14T09:29:56.892Z",
"FINISHED_AT": "2026-09-14T09:29:57.968Z",
"DURATION_MS": 1075,
"EXIT_CODE": "COMPLETED"
},
...
]
```
Full transcript, all four partitions: [`docs/output/05-happy-path-4-partitions.txt`](output/05-happy-path-4-partitions.txt).
Two things this makes concrete that configuration alone does not: partition-to-thread assignment
is not 1:1 with partition *number* (`partition0` landed on thread `order-partition-2` here, not
`order-partition-1` — `TaskExecutorPartitionHandler` submits tasks in the order the
`Partitioner`'s map iterates, and `ThreadPoolTaskExecutor` hands them to whichever pooled thread is
free first); and all four really did start within milliseconds of each other, which is the
difference between "partitioned" as a configuration property and "partitioned" as an observed
fact.
This same table is what makes [chapter 7](07-the-rejectedexecutionexception.md) and
[chapter 8](08-restart-reruns-only-the-failed-partition.md) possible to write with confidence:
querying `PARTITION_STATS` (or, for those chapters, the framework's own `BATCH_STEP_EXECUTION`
table directly) is how this article found that three rejected partitions do not fail, they hang.
**Delete this before shipping.** An unauthenticated endpoint that dumps job-internal timing data
belongs behind whatever authentication and authorization the rest of the service already has, at
minimum, and arguably behind nothing publicly reachable at all — it exists here to make the
mechanism visible for this article, not because a production batch service should expose it.
## Going deeper
- `StepExecutionListener` and the other listener interfaces available:
[Spring Batch reference — Listeners](https://docs.spring.io/spring-batch/reference/step/chunk-oriented-processing/configuring-a-step.html) (`rel="nofollow"`).
[Next: Why CPU-bound, not I/O-bound →](06-why-cpu-bound-not-io-bound.md)