Apply UpdateQuantity to the live maker state (#115)#128
Conversation
Codecov Report❌ Patch coverage is
... and 1 file with indirect coverage changes 🚀 New features to boost your workflow:
|
joaquinbejar
left a comment
There was a problem hiding this comment.
The live entry decision fixes the stale quantity and priority race from #127, and the focused #115 tests pass, but the new counter decrease can wrap during a replenish race and the resurrection stress test can pass without exercising that interleaving. The inherited index rekey and destructive pop Critical findings already filed on #127 also remain unresolved. The intended effective verdict is REQUEST_CHANGES, submitted as COMMENT because the active account owns this PR.
1f7e842 to
eed1cfc
Compare
joaquinbejar
left a comment
There was a problem hiding this comment.
The live maker derivation remains correct, but the existing counter underflow and race coverage warnings plus the inherited queue rekey and pop blockers remain unresolved. The intended effective verdict is REQUEST_CHANGES. GitHub requires COMMENT because this is a self review.
eed1cfc to
1f33e15
Compare
joaquinbejar
left a comment
There was a problem hiding this comment.
The live maker derivation resolves the stale priority decision, but the existing counter underflow race and weak interleaving test remain, along with the inherited index rekey and destructive pop blockers. The intended effective verdict is REQUEST_CHANGES. GitHub requires COMMENT because this is a self review.
joaquinbejar
left a comment
There was a problem hiding this comment.
The current head fixes live maker derivation, but the existing counter underflow and race coverage warnings remain, along with inherited queue rekey and destructive pop blockers. The intended effective verdict is REQUEST_CHANGES. GitHub requires COMMENT because this is a self review.
1f33e15 to
1a380b0
Compare
joaquinbejar
left a comment
There was a problem hiding this comment.
The current head fixes live maker derivation and stale priority selection, but the existing counter underflow race and ineffective interleaving test remain, together with the inherited index rekey and destructive pop blockers. The intended effective verdict is REQUEST_CHANGES. GitHub requires COMMENT because this is a self review.
joaquinbejar
left a comment
There was a problem hiding this comment.
Deriving the update from the live maker fixes the stale priority decision, but the inherited rekey and pop races remain, the unchecked counter decrease can wrap during replenishment, and the race test does not force that interleaving. GitHub requires this self review to be submitted as COMMENT, but the intended effective verdict is REQUEST_CHANGES.
The quantity-increase branch demoted via remove(id) + push(new): during that gap the id was absent from the queue map, so a concurrent cancel reported no removal while the update re-inserted the maker (lost cancel / resurrection), a concurrent same-id admission could win try_push and then be blindly overwritten (the P1 flagged in the #113 audit), and the transient absence was the second side-topology race window documented in #120. match_front also never verified that the index-selected sequence still owned the live map entry, so a stale front selection could act on a just-demoted maker. New OrderQueue::resequence_to_tail extends the ReplaceAtTail shape: under one continuous hold of the DashMap entry lock it mints a tail sequence, swaps the stored (seq, order) pair in place, and re-keys the index -- the id never leaves the map. A concurrent cancel now either fully precedes (update reports not-found) or fully follows (it removes the re-sequenced order); a same-id admission is always rejected as DuplicateOrderId. match_front gains a stale-seq guard: if the stored sequence no longer equals the index-selected one, the stale key is dropped and the scan retries -- sequences are monotonic and never reused, so the removal is provably safe. Demotion semantics unchanged (fresh tail seq, original timestamp; #109 tests untouched). BREAKING: OrderQueue::push (blind overwrite) has no production caller left and is retired to #[cfg(test)] -- admission uses try_push, demotion uses resequence_to_tail. Migration guide + README updated; the now-closed #113/#120 doc caveats are retired. Barrier race tests (1500 iterations each): upsize-vs-cancel never loses a cancel or resurrects; upsize-vs-duplicate-admission is always rejected with no counter drift (fails 3/3 under the old remove+push); concurrent upsize+match stress drains consistently. Perf: update-heavy bench 26.0us vs 25.7us median -- neutral within noise; the win is correctness. Closes #119
1a380b0 to
e1ca5eb
Compare
- resequence_to_tail and the match sweep's ReplaceAtTail now publish the new index key BEFORE removing the old one, so a continuously resident maker is never transiently absent from the ordered index and a concurrent front scan can never report Empty past resting liquidity; the transient two-key window is discarded on selection by the stale-front guard (the stored sequence is already the new one) - pop_entry validates, under the map entry, that the stored sequence equals the popped index key; a demoted maker's stale old key is dropped and the scan retries, so the destructive pop can neither return a just-demoted maker ahead of older makers nor strand its new key - the allocation-free claim on the re-key path is narrowed: the SkipMap insert allocates the new index node Stress + deterministic tests: 200k front scans against a continuous demoter never observe Empty; a demoted maker drains last and exactly once under a racing pop; map/index end empty and consistent.
joaquinbejar
left a comment
There was a problem hiding this comment.
The live maker derivation fixes stale priority selection, but the existing counter underflow race and ineffective interleaving test remain, together with the inherited index rekey and destructive pop blockers. GitHub requires this self review to be submitted as COMMENT, but the intended effective verdict is REQUEST_CHANGES.
UpdateQuantity derived the replacement and its priority policy from a stale pre-read: a concurrent match could consume or replenish the maker between the read and the swap, so the stale replacement resurrected already-executed visible or hidden quantity (iceberg hidden depth included), and the stale total could pick the wrong priority branch. Counters stayed numerically consistent while describing resurrected liquidity, and a snapshot persisted the corruption durably. New OrderQueue::update_entry runs the decide closure against the LIVE stored order under the per-entry lock and commits its UpdateDecision (in-place swap, or inline tail re-sequence -- a separate call would double-lock the shard) in the same critical section. The level closure resizes from the live order (new_quantity applies to the live visible tranche, hidden preserved as live -- resurrection is structurally impossible), decides increase-vs-decrease from the live total, and reserves the level-counter delta with fetch_update(checked_add) BEFORE the queue commits: an update whose delta would overflow a level counter is rejected with InvalidOperation leaving maker, position, and counters untouched. That closes the fetch_add(new - old) wrap flagged in the issue-111 audit. update_in_place and resequence_to_tail are subsumed and removed. Race tests (Barrier, 1000 iterations each): competing same-id updates leave exactly one winner with consistent counters; decrease-vs-cancel never resurrects; update-vs-match on a replenishing iceberg never raises hidden above its pre-race value; plus a deterministic level-counter overflow rejection and a live-derivation sanity case. The shared consistency helper now also asserts the LIVE advisory counters converge to the queue sums when quiescent (previously it only compared snapshot folds, which are consistent by construction). Perf: update-heavy bench 24.12us vs 24.97us median -- ~3% faster (one entry-lock acquisition instead of a pre-read plus a separate commit). Closes #115
e1ca5eb to
b097f8b
Compare
There was a problem hiding this comment.
The shared queue fixes resolve the inherited rekey and destructive pop blockers, and deriving the update from the live maker fixes stale priority selection. The counter decrease can still underflow during a replenish race, and the stress test still does not force that interleaving. GitHub requires this self review to be submitted as COMMENT, but the intended effective verdict is REQUEST_CHANGES.
- the match sweep's replenish branch now publishes its level-counter transition inside the locked decision closure -- one checked fetch_update applying the net visible delta plus the hidden draw -- so the counters can never transiently lag a committed replenishment and a concurrent UpdateQuantity decrease can no longer underflow; the post-lock sweep body skips the already-committed step (flag threaded through StepData) and the update path's decrease reserve gains a checked_sub defense documented as unreachable once the structural ordering holds - the old order Arc is moved out with mem::replace in update_entry and match_front's in-place / re-key arms and dropped only after the shard guard releases, so a last-reference deallocation never runs inside the critical section - the update-vs-replenish race test now asserts strictly non-increasing hidden depth while the maker rests (the pre-#115 stale write-back fails it), guarantees matcher progress, and runs the drain across the whole race window
Summary
UpdateQuantityderived the replacement order and its priority policy from astale pre-read, so a concurrent match could execute quantity that the update
then silently restored — including hidden iceberg depth — with numerically
consistent counters describing resurrected liquidity, durably persisted by
snapshots. The read/derive/decide/replace sequence is now one atomic operation
against the live stored order.
Stack
stack/7-119-atomic-reseqmainis cumulative; review against the base branch.Changes
OrderQueue::update_entry(order_id, decide)+UpdateDecisionenum(mirrors the
match_frontdecision-closure precedent):deciderunsagainst the live stored order under the per-entry lock; its decision
(in-place swap or inline tail re-sequence) commits in the same critical
section.
new_quantity→ livevisible tranche, hidden preserved as live — resurrection structurally
impossible), picks increase-vs-decrease from the live total, and reserves
the level-counter delta with
fetch_update(checked_add)before thequeue commits — an overflowing delta rejects the update with the level
untouched (closes the
fetch_add(new − old)wrap flagged in Reject quantity overflow before mutating level state (#111) #124's audit).update_in_placeandresequence_to_tailsubsumed and removed(
pub(crate)— no public impact).UpdatePriceAndQuantity's same-pricebranch routes through the same primitive.
Technical decisions
concurrency-auditor: lock-free safe, commit safe, 0 P1/P2 — single-lockdesign cannot deadlock (only one lock exists; atomics/SkipMap are
lock-free), the production closure is panic-free with all fallible work
before the first counter write, and the price-level: Resequence quantity increases without exposing an absent order #119 orphan-free re-key argument
transfers to the inlined body. Its test-adequacy finding (the consistency
helper compared snapshot folds — consistent by construction) is fixed: the
helper now also asserts the live advisory counters converge to the
queue sums when quiescent.
architect's#[must_use]and id-guard(
debug_assert) notes applied.Performance
Update-heavy bench (
Update Orders/update_quantity, 50 upsizes/iter):24.12 µs [24.05, 24.19] vs 24.97 µs [24.89, 25.04] base — ~3% faster
(one entry-lock acquisition instead of a pre-read plus a separate commit).
Testing
(one winner, counters == queue), decrease-vs-cancel (never resurrected),
update-vs-match on replenishing iceberg (hidden never exceeds pre-race
value, counters == queue == snapshot)
fetch_addwouldhave wrapped and succeeded)
hidden, never the pre-fill value
snapshot folds
489 tests passed; 0 failed (470 lib + 9 + 1 + 9 doctests).
Checklist
rules/global_rules.mdandCLAUDE.md.unwrap()/.expect()/ unchecked indexing in production codeSeqCstorderings justified (advisory counters; happens-before onthe entry lock)
unsafe, no new dependenciesCloses #115