Files
spring-boot-demo/spring-batch-partitioning/docs/05-the-diagnostic-endpoint.md
T

3.4 KiB

5. The diagnostic endpoint: proving four threads ran, not just configuring them

← Previous | README | Next: Why CPU-bound, not I/O-bound →

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 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 exposes that table over HTTP:

$ 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. 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-1TaskExecutorPartitionHandler 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 and chapter 8 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

Next: Why CPU-bound, not I/O-bound →