fix: edge relay hardening (gapped replay, snapshot starvation, cold start, trades race) (#99)#111
Merged
Merged
Conversation
…d start, and the trades race (#99) Four related, mostly self-healing weaknesses in the market-relay edge path, hardened now that the relay sits in the live path. 1. Snapshot starvation (EdgePublisher). The periodic bundle/snapshot frames are the relay's only resync path (they reset snapshotVersion and trim its delta buffer), yet they shared the single drop-oldest queue with live deltas and could be evicted under a spike before being sent. Route snapshot/bundle frames to a separate bounded priority lane that deltas cannot evict and that drains first. Both lanes stay bounded, so memory stays bounded. 2. Gapped-buffer refresh (feed.ts). Track each delta's fromVersion. When the earliest surviving delta cannot chain from the snapshot (from > snapshotVersion), drop the buffer and serve snapshot-only instead of replaying a hole the client would just re-refresh on. This covers both the refresh path and subscribe. 3. Cold-start blind-apply (feed.ts). Never replay buffered deltas on top of the empty synthetic v0 snapshot; until a real snapshot exists for a market, serve the empty snapshot alone. 4. Trades bundle-replace race (feed.ts). Merge the authoritative bundle tape into the live-teed one by trade key and keep the newest N, instead of a wholesale replace that could clobber a newer live trade queued just ahead of a slightly-older bundle. Tests: - EdgePublisherTest: the priority lane retains a bundle snapshot under a delta flood that evicts the normal lane, routes live BOOK_SNAPSHOT frames, and stays bounded while keeping the freshest snapshot. - feed.spec.ts: cold start serves the empty snapshot alone (no delta replay); a gapped buffer makes refresh serve snapshot-only; a chaining buffer still replays; a stale bundle no longer clobbers a newer live trade. Closes #99 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hardens the market-relay edge path against four related, mostly self-healing weaknesses found in the match#84 bug-hunt (issue #99), now that the relay is in the live path. Each fix is minimal and keeps the existing best-effort, bounded-memory semantics.
1. Snapshot starvation (drop-oldest can evict the repair snapshot)
EdgePublisher.publishdropped from the head of a single 8192 queue under pressure with no frame priority, so the periodic bundle/snapshot frame (the only thing that resets the relay'ssnapshotVersionand trims its delta buffer) could be evicted before it shipped.Fix: route snapshot/bundle frames (EDGE_CACHE envelopes and any live
BOOK_SNAPSHOT) to a separate bounded priority lane.drainflushes that lane first, and live deltas can never evict it. The delta lane keeps its drop-oldest "fresh state wins" behaviour. Both lanes are bounded, so memory stays bounded. Deltas are never reordered among themselves (single FIFO lane), so only a snapshot can jump ahead of an older delta, which is safe: the relay drops or chains it correctly.2. refresh re-serves a known-gapped buffer
A
refreshwas identical tosubscribeand re-sent the same snapshot plus a delta buffer whose earliest surviving entry could start past the snapshot, so the client just re-refreshed in a loop until the next clean bundle.Fix: each delta now records its
fromVersion. When the earliest surviving delta cannot chain from the snapshot (from > snapshotVersion),sendBookdrops the buffer and serves snapshot-only. This covers bothrefreshandsubscribe.3. Cold-start blind-applies deltas
If a market's first frames were
BOOK_DELTAs (before any snapshot),sendBooksentemptyBookSnapshot(v0)followed by the whole delta buffer, which the UI'sbookVersion===0branch applies with no chain gate, leaving the book permanently wrong with no resync.Fix: while no real snapshot exists for a market, serve the empty snapshot alone and never replay buffered deltas onto it.
4. Trades bundle-replace races the live tee
enqueueCacheBundlesbuilds an authoritative trades tape on another thread; a wholesale replace in the relay could clobber a newer live-teed trade queued just ahead of a slightly-older bundle.Fix: merge the bundle tape into the live tape by trade key (timestamp/price/quantity/side) and keep the newest N, instead of replacing wholesale. The live-append path is unchanged.
Tests
TypeScript (
edge/market-relay,npx vitest run— 17 passed):refreshserve snapshot-only (noBOOK_DELTA)Java (
match-gateway,EdgePublisherTest— 3 passed):BOOK_SNAPSHOTis routed to the priority lane, a delta is notCloses #99
🤖 Generated with Claude Code