test: make RobustnessTest.testConcurrentUsage deterministic #9

Merged
KofTwentyTwo merged 1 commits from feature/robustness-test-flake into develop 2026-10-12 21:17:17 +00:00
KofTwentyTwo commented 2026-10-12 21:13:29 +00:00 (Migrated from github.com)

Summary

Fixes the flaky RobustnessTest.testConcurrentUsage (CircleCI job 22: expected: 20 but was: 19).

Root cause

The job's output shows a writer thread dying before the assertion:

java.lang.RuntimeException: QException: Error executing query
  at AssistantStore.update -> UpdateAction.fetchOldRecords -> MemoryQueryAction
Caused by: java.util.ConcurrentModificationException
  at java.util.HashMap$HashIterator.nextNode
  at MemoryRecordStore.query(MemoryRecordStore.java:317)

AssistantStore.increment locks per key (scope|userId|date). One writer updating the INSTANCE row (INSTANCE lock) and another inserting the first OTHER_EDITOR row (a different lock) touch the same in-memory table at once. QQQ's MemoryRecordStore.query iterates the table's Collections.synchronizedMap(...).values() without holding the map's lock, while insert mutates it, so the iteration can throw. The virtual thread swallowed the exception, and the assertion only saw one lost count.

This is a race in the in-memory test backend, not in the product. On a real database each call is atomic, and AssistantStore's per-key locks plus the unique key handle the read-modify-write. No quarantine needed.

Fix (test only)

  • The writers use AtomicCallsStore, an AssistantStore whose individual query / insert / update calls are serialized, as a database's are. The read-modify-write across calls is still protected only by AssistantStore's per-key locks, which is what the test checks.
  • A start latch releases all 20 writers together, so the test applies real contention.
  • An executor's futures surface any writer exception as a test failure, so it can't vanish again.

Verification

  • With the start latch and the plain AssistantStore, the CME reproduced locally in 2 of 6 runs. With AtomicCallsStore, 12 of 12 runs passed.
  • ./mvnw verify: BUILD SUCCESS, all modules, coverage checks met.

Side note: the unsynchronized iteration in MemoryRecordStore.query (and the other getTableData(...).values() loops) is a QQQ memory-backend issue that could be fixed upstream.

🤖 Generated with Claude Code

## Summary Fixes the flaky `RobustnessTest.testConcurrentUsage` ([CircleCI job 22](https://circleci.com/gh/QRun-IO/qbit-assistant/22): `expected: 20 but was: 19`). ## Root cause The job's output shows a writer thread dying before the assertion: ``` java.lang.RuntimeException: QException: Error executing query at AssistantStore.update -> UpdateAction.fetchOldRecords -> MemoryQueryAction Caused by: java.util.ConcurrentModificationException at java.util.HashMap$HashIterator.nextNode at MemoryRecordStore.query(MemoryRecordStore.java:317) ``` `AssistantStore.increment` locks per key (`scope|userId|date`). One writer updating the INSTANCE row (INSTANCE lock) and another inserting the first OTHER_EDITOR row (a different lock) touch the same in-memory table at once. QQQ's `MemoryRecordStore.query` iterates the table's `Collections.synchronizedMap(...).values()` without holding the map's lock, while `insert` mutates it, so the iteration can throw. The virtual thread swallowed the exception, and the assertion only saw one lost count. This is a race in the in-memory test backend, not in the product. On a real database each call is atomic, and AssistantStore's per-key locks plus the unique key handle the read-modify-write. No quarantine needed. ## Fix (test only) - The writers use `AtomicCallsStore`, an `AssistantStore` whose individual `query` / `insert` / `update` calls are serialized, as a database's are. The read-modify-write across calls is still protected only by AssistantStore's per-key locks, which is what the test checks. - A start latch releases all 20 writers together, so the test applies real contention. - An executor's futures surface any writer exception as a test failure, so it can't vanish again. ## Verification - With the start latch and the plain `AssistantStore`, the CME reproduced locally in 2 of 6 runs. With `AtomicCallsStore`, 12 of 12 runs passed. - `./mvnw verify`: BUILD SUCCESS, all modules, coverage checks met. Side note: the unsynchronized iteration in `MemoryRecordStore.query` (and the other `getTableData(...).values()` loops) is a QQQ memory-backend issue that could be fixed upstream. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.