Skip to content

feat(outbox-store): Use executeBatch() after processing entries instead of single executeUpdate() for each entry#71

Open
rawo wants to merge 5 commits into
mainfrom
feat/kojak-75-use-execute-batch
Open

feat(outbox-store): Use executeBatch() after processing entries instead of single executeUpdate() for each entry#71
rawo wants to merge 5 commits into
mainfrom
feat/kojak-75-use-execute-batch

Conversation

@rawo

@rawo rawo commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Implements KOJAK-75: replaces N individual updateAfterProcessing() calls per outbox batch with a single batched JDBC write, cutting per-batch DB roundtrips from N to 1.

  • OutboxStore.updateAfterProcessingBatch(entries) — new method with a default implementation that loops updateAfterProcessing() one at a time, so existing implementations (test stubs, future stores) don't break.
  • PostgresOutboxStore / MysqlOutboxStore override it with a single PreparedStatement reused across all entries via addBatch() / executeBatch(), touching only the 5 mutable columns (status, updated_at, retries, last_attempt, last_error) rather than the full 11-column upsert persist() uses. Postgres binds the id with setObject(uuid); MySQL binds it with setString(uuid.toString()) (CHAR(36) column, no native UUID type).
  • OutboxProcessor.processNext() now calls store.updateAfterProcessingBatch(processed) once per drained batch instead of looping updateAfterProcessing() per entry.

Benchmark

New OutboxStoreUpdateBatchBenchmark (JMH) isolates the update phase for 1000 entries:

Method ms/op (per entry) Total for 1000 entries Improvement
individualUpdates 0.188 ± 0.008 ~187.6 ms baseline
batchUpdate 0.018 ± 0.002 ~18.3 ms 10.2×

Full writeup and raw JSON in benchmarks/results-postopt-KOJAK-75.md.

Test plan

  • Unit tests: OutboxStoreTest (default updateAfterProcessingBatch loop behavior, incl. empty list), OutboxProcessorTest (asserts processNext() calls the batch method exactly once, never per-entry)
  • Integration tests: OutboxStoreContractTests — mixed final statuses (DELIVERED + PENDING retry + FAILED) persisted via one updateAfterProcessingBatch() call, run against both Postgres and MySQL
  • ./gradlew ktlintCheck clean
  • ./gradlew build green (core, postgres, mysql, micrometer, integration-tests, spring-boot)

Refs: KOJAK-75 (blocked-by KOJAK-73, already merged — deliverBatch fire-flush-await)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements KOJAK-75 by adding a batch update API to OutboxStore and switching the processing pipeline to persist delivery results using a single JDBC executeBatch() call per drained outbox batch, reducing DB roundtrips from N to 1 for the “update-after-processing” phase.

Changes:

  • Added OutboxStore.updateAfterProcessingBatch(entries) with a default per-entry fallback implementation.
  • Implemented optimized JDBC batch updates in PostgresOutboxStore and MysqlOutboxStore, updating only the mutable delivery-state columns.
  • Updated OutboxProcessor.processNext() to call the new batch update method once per processed batch; added unit + integration tests plus a JMH benchmark and benchmark artifacts.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
okapi-postgres/src/main/kotlin/com/softwaremill/okapi/postgres/PostgresOutboxStore.kt Adds Postgres batch UPDATE via addBatch()/executeBatch() and extracts SQL strings to companion object.
okapi-mysql/src/main/kotlin/com/softwaremill/okapi/mysql/MysqlOutboxStore.kt Adds MySQL batch UPDATE via addBatch()/executeBatch() and extracts SQL strings to companion object.
okapi-integration-tests/src/test/kotlin/com/softwaremill/okapi/test/store/OutboxStoreContractTests.kt Adds contract test verifying mixed-status batch persistence works end-to-end.
okapi-core/src/test/kotlin/com/softwaremill/okapi/core/OutboxStoreTest.kt Adds unit tests for default updateAfterProcessingBatch fallback behavior.
okapi-core/src/test/kotlin/com/softwaremill/okapi/core/OutboxProcessorTest.kt Adds unit test asserting the processor calls the batch method exactly once and never per-entry.
okapi-core/src/main/kotlin/com/softwaremill/okapi/core/OutboxStore.kt Introduces the new default batch update method on the store interface.
okapi-core/src/main/kotlin/com/softwaremill/okapi/core/OutboxProcessor.kt Switches persistence step from per-entry updates to a single batch update call.
okapi-benchmarks/src/jmh/kotlin/com/softwaremill/okapi/benchmarks/OutboxStoreUpdateBatchBenchmark.kt Adds JMH benchmark isolating the update phase and comparing individual vs batch updates.
benchmarks/results-postopt-KOJAK-75.md Documents benchmark methodology and results for the batch update optimization.
benchmarks/outbox-store-update-batch.json Stores raw JMH output for the reported benchmark run.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread okapi-mysql/src/main/kotlin/com/softwaremill/okapi/mysql/MysqlOutboxStore.kt Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Comment thread benchmarks/outbox-store-update-batch.json Outdated

@ramafasa ramafasa left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants