Skip to content

test(ledger): prove the no-lost-update invariant under concurrent posting - #151

Merged
tiana-code merged 2 commits into
mainfrom
feat/E-01-concurrent-posters
Jun 17, 2026
Merged

test(ledger): prove the no-lost-update invariant under concurrent posting#151
tiana-code merged 2 commits into
mainfrom
feat/E-01-concurrent-posters

Conversation

@tiana-code

Copy link
Copy Markdown
Owner

What

Adds ConcurrentPostersIT, a Testcontainers PostgreSQL 17 concurrency test that proves the ledger's no-lost-update invariant under real contention. 100 virtual-thread posters, released together by a CountDownLatch start-gate, each commit a distinct balanced DEBIT +100.00 / CREDIT -100.00 transaction to one shared account pair through the idempotency-wrapped path (idempotencyService.execute { transactionService.post(...) }), the only path where the bounded optimistic-retry absorbs the @Version conflicts on the shared account_balances rows.

Asserted contract (non-flaky, S = observed success count)

  • Final balance of each account equals the algebraic sum of the committed postings (balance == 100.00 * S via compareTo).
  • Every poster resolves to exactly one outcome: committed success or a typed ConcurrencyConflictException; any other throwable (including a raw OptimisticLockingFailureException or a pool-timeout) is captured first and fails the test loudly.
  • Transaction rows == S; entry rows == 2 * S; vacuous-run guard (balance != 0 when S >= 1).

All assertions are phrased over the observed S, so the test passes on every CI run regardless of how the scheduler resolves 100-way contention (default max-attempts=3, no backoff, means many posters legitimately resolve to a typed 503; that is correct and asserted).

Notes

  • Test-only slice, zero production change.
  • @DataJpaTest + @Transactional(NOT_SUPPORTED) so worker threads commit and the read-back sees committed cross-thread state; mirrors the AuditRetryTopologyIT harness.
  • Hikari pool sized (maximum-pool-size=16, explicit connection-timeout) to keep real parallelism while staying within the shared-container budget; @AfterEach outboxRepository.deleteAll() keeps sibling global-count ITs green.
  • Runs on CI only (Testcontainers; no in-memory fallback).

Closes #56

@tanya_r added 2 commits June 16, 2026 21:53
…ting

Add ConcurrentPostersIT: 100 virtual-thread posters commit distinct
balanced transactions to one shared account pair through the
idempotency-wrapped path, where the bounded optimistic-retry absorbs the
@Version conflicts on the shared account_balances rows. Asserts the final
balance equals the algebraic sum of the committed postings, every failure
is a typed ConcurrencyConflictException with no raw optimistic-lock leak,
and entry/transaction row counts match the success count.

The contract is asserted over the observed success count so the test is
non-flaky regardless of how the scheduler resolves contention. Test-only
slice, no production change.

Closes #56
Isolate the optimistic UPDATE+retry contention path from the cold-start
account_balances INSERT race (filed as #152), which the retry loop does
not cover. Adjust expected counts to include the warm-up posting.
@tiana-code
tiana-code merged commit c77659a into main Jun 17, 2026
8 checks passed
@tiana-code
tiana-code deleted the feat/E-01-concurrent-posters branch June 17, 2026 01:10
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.

Concurrency test: 100 concurrent posters

1 participant