Measured on the same machine/config as results-kafka-deliverbatch.md:
JDK 21 LTS, Postgres 16 via Testcontainers, full JMH config fork=2, warmup=3 × 10s, iter=5 × 30s
(n=10 samples per benchmark).
OutboxStoreUpdateBatchBenchmark isolates the persistence step: 1000 entries are claimed and
pre-computed as already-processed (toDelivered) in @Setup(Level.Invocation), then each
@Benchmark method times only the write-back — either N individual
updateAfterProcessing() calls or a single updateAfterProcessingBatch() executeBatch() call.
| Method | ms/op (per entry) | Total for 1000 entries | Improvement |
|---|---|---|---|
individualUpdates |
0.1876 ± 0.0080 | ~187.6 ms | baseline |
batchUpdate |
0.0183 ± 0.0020 | ~18.3 ms | 10.2× |
Raw JSON: outbox-store-update-batch.json.
OutboxStore.updateAfterProcessingBatch(entries)— new method, default implementation loopsupdateAfterProcessing()(one JDBC roundtrip per entry, unchanged behavior for stores that don't override it).PostgresOutboxStore/MysqlOutboxStoreoverride it with a singlePreparedStatementreused across all entries viaaddBatch()/executeBatch(), touching only the 5 mutable columns (status,updated_at,retries,last_attempt,last_error) instead of the full 11-column upsert used bypersist().OutboxProcessor.processNext()now callsstore.updateAfterProcessingBatch(processed)once per drained batch instead of loopingupdateAfterProcessing()per entry.
- 10.2× on localhost Postgres (Testcontainers, ~sub-ms RTT) is already above the ticket's 50-70% relative improvement estimate — expected, since that estimate was for the whole Kafka-delivery batch (deliver + update), whereas this benchmark isolates the update phase alone, which is exactly where all the savings land.
- Real-world gains should be larger, not smaller: this machine's Postgres RTT is well under
1ms, so the "N roundtrips" baseline is already best-case. Against a real network-hosted
Postgres (1-5 ms RTT),
individualUpdateswould scale roughly linearly with N × RTT whilebatchUpdatestays close to one roundtrip — the gap should widen further. - Error bars (
±above) are small relative to the score for both benchmarks, so the 10.2× figure is not noise.
This run also re-executed KafkaThroughputBenchmark / HttpThroughputBenchmark /
DelivererMicroBenchmark as part of the same :okapi-benchmarks:jmh invocation (a benchmark
filter that was expected to narrow the run to OutboxStoreUpdateBatchBenchmark alone was not
honored by the me.champeau.jmh plugin). The full run's wall clock was affected by an
environment interruption partway through (host suspend/resume), so the KafkaThroughputBenchmark
/ HttpThroughputBenchmark numbers from results.json in this run are not directly
comparable to the KOJAK-73 baseline and should be re-measured
in a clean run before drawing conclusions about end-to-end throughput improvement. Only the
OutboxStoreUpdateBatchBenchmark numbers above — captured within that same run but validated by
tight, consistent error bars across both forks — are reported here.
- Unit tests:
OutboxStoreTest(defaultupdateAfterProcessingBatchloop behavior),OutboxProcessorTest(assertsprocessNext()calls the batch method exactly once instead of per-entry). - Integration tests:
OutboxStoreContractTests— mixed final statuses (DELIVERED + PENDING retry + FAILED) persisted via oneupdateAfterProcessingBatch()call, run against both Postgres and MySQL. - ktlint clean, full
./gradlew buildgreen (core, postgres, mysql, micrometer, integration-tests, spring-boot).