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.
## 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)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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:
AssistantStore.incrementlocks 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'sMemoryRecordStore.queryiterates the table'sCollections.synchronizedMap(...).values()without holding the map's lock, whileinsertmutates 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)
AtomicCallsStore, anAssistantStorewhose individualquery/insert/updatecalls 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.Verification
AssistantStore, the CME reproduced locally in 2 of 6 runs. WithAtomicCallsStore, 12 of 12 runs passed../mvnw verify: BUILD SUCCESS, all modules, coverage checks met.Side note: the unsynchronized iteration in
MemoryRecordStore.query(and the othergetTableData(...).values()loops) is a QQQ memory-backend issue that could be fixed upstream.🤖 Generated with Claude Code