fix(xmtp_api_d14n): close two bidi ledger loss holes under overlapping catch-up waves#3852
fix(xmtp_api_d14n): close two bidi ledger loss holes under overlapping catch-up waves#3852tylerhawkes wants to merge 1 commit into
Conversation
|
Claude finished @tylerhawkes's task —— View job PR Review — 2026-07-14 06:23 UTCReviewing: fix(xmtp_api_d14n): close two bidi ledger loss holes under overlapping catch-up waves
SummaryThis is a well-documented, critical correctness fix addressing two subtle message-loss bugs in the bidi transport ledger. The implementation is sound and thoroughly tested. The author has already addressed previous review concerns about unbounded buffers and message reordering. Key Strengths
Minor ObservationsCode Quality: The Edge Case Handling: The implementation correctly handles:
Performance: Arc-cloning for withheld frames is efficient — one allocation per wire frame shared across all withholding leases, with Test Coverage: 82.44% patch coverage with 56 missing lines. The uncovered lines appear to be error paths (e.g., telemetry, defensive checks). Given the thorough scripted and proptest coverage, this is acceptable. Verdict✅ Approve — This fix correctly addresses real message-loss bugs with proper bounds, comprehensive testing, and clear documentation. No blocking issues found. |
ApprovabilityVerdict: Needs human review 1 blocking correctness issue found. This PR introduces significant runtime behavior changes to the bidirectional transport ledger (new withheld-frame buffering, new state fields, new delivery semantics). An unresolved high-severity comment identifies a potential bug where the flush logic could deterministically drop leases during normal catch-up traffic due to bounded channel capacity. You can customize Macroscope's approvability policy. Learn more. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3852 +/- ##
==========================================
- Coverage 85.48% 85.40% -0.09%
==========================================
Files 413 413
Lines 65636 66012 +376
==========================================
+ Hits 56112 56379 +267
- Misses 9524 9633 +109 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
4b8388e to
977d84a
Compare
|
Addressed the review round in 977d84a:
Suite 233/233 (including the new overflow test), proptest model + live fuzz green on the updated ledger. The browser-sdk shard failure on the previous run is the same flake family currently failing on |
977d84a to
11acadd
Compare
| flushed.sort_unstable_by_key(|(seq, _)| *seq); | ||
| let mut groups: Vec<B::GroupMessage> = Vec::new(); | ||
| let mut welcomes: Vec<B::WelcomeMessage> = Vec::new(); | ||
| for (_, frame) in flushed { |
There was a problem hiding this comment.
🟠 High queries/bidi_transport.rs:1044
complete flushes withheld frames by emitting a separate channel event for each kind alternation (a WelcomeMessages event, then a GroupMessages event, then WelcomeMessages, etc.). Because deliver uses bounded try_send, a lease with 65 alternating group/welcome frames and a channel capacity of 64 is deterministically dropped mid-flush — the consumer cannot drain during this synchronous loop, so normal catch-up traffic triggers the wedge-recovery path and terminates the stream instead of completing. Consider coalescing all withheld frames of the same kind into a single event before sending, or batching the flush into one event per kind.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @crates/xmtp_api_d14n/src/queries/bidi_transport.rs around line 1044:
`complete` flushes withheld frames by emitting a separate channel event for each kind alternation (a `WelcomeMessages` event, then a `GroupMessages` event, then `WelcomeMessages`, etc.). Because `deliver` uses bounded `try_send`, a lease with 65 alternating group/welcome frames and a channel capacity of 64 is deterministically dropped mid-flush — the consumer cannot drain during this synchronous loop, so normal catch-up traffic triggers the wedge-recovery path and terminates the stream instead of completing. Consider coalescing all withheld frames of the same kind into a single event before sending, or batching the flush into one event per kind.
Follow-up to #3845 (merged before these landed on the branch). The deterministic proptest model built for #3850 found two real message-loss bugs in the transport ledger, both reachable only while two catch-up waves overlap on one topic — a milliseconds-wide window against a live node that the mock-wire model holds open. Both are silent losses: the frames are simply never delivered to the affected lease.
Bug 1 — virgin-topic snapshot hole
A lease registering a topic the transport had never delivered a frame for (
last_seenempty) records no registration snapshot, and the owed-history lane read "no snapshot" as nothing predated this lease. If any overlapping wave's replay then advanced the wire position, the lease's own replay arrived non-fresh, hit the owed lane, and was skipped wholesale — everything below the overlap lost.Realistic trigger: cold start. The catch-up pass and a stream subscription lease the same group back-to-back on a fresh process; the first wave's replay races ahead of the second's.
Minimal repro (proptest-shrunk, now a scripted regression —
overlapping_waves_on_a_virgin_topic_lose_nothing): topic with history[1..4]never seen by the transport; lease A at floor 2, lease B at floor 0; A's wave replays(2,4]first; B's wave replays(0,4]— B never received1,2.Bug 2 — cross-wave watermark gap-jump
The owed lane is deliberately tag-agnostic (a lower re-add moves a topic into the re-adder's wave, so any wave may carry a lease's history). But the per-(lease, topic)
replayedwatermark max-folds across waves with different floors: a wave that asked from a higher floor — or one that had already replayed past the lease's floor before the lease registered — delivers its frames, the watermark jumps over the gap underneath, and the lease's own replay of that gap is then "covered" and skipped. Lost for good.The fix
CatchUpComplete. The one exception that keeps catch-up streaming: on an open owed range (no snapshot), frames of a wave the lease itself rides are its replay and deliver immediately, draining any withheld frames below them first.reconnect_planre-asks from the floor (pushed past the watermark) instead of the wire position.max(holder floor, watermark), and the wave must not predate the holder's registration (preexistingset — what remains of an older wave's replay is unverifiable). Skipped frames re-arrive inside the holder's own wave, in order.Net effect: per-lease per-topic delivery is now strictly increasing — order, exactly-once, and completeness in one invariant — instead of exactly-once-but-occasionally-reordered-or-lossy.
One file: the ledger demux/registration/reconnect in
bidi_transport.rs, plus module docs, two new proptest-shrunk scripted regressions, and three existing scripted tests tightened to the stronger contract (they previously asserted the raced fresh delivery).Testing
xmtp_api_d14nscripted suite: 233/233 (incl. the two new regressions).🤖 Generated with Claude Code
https://claude.ai/code/session_017AVuJ7VAgen2fFhtJQ6zid
Note
Fix ledger loss holes in bidi transport under overlapping catch-up waves
CatchUpComplete.reconnect_planto resume a virgin-topic catch-up from the lease floor advanced by replayed progress, not from the wire position, preventing data loss on overlapping waves.WITHHELD_FRAMES_CAP = 512per lease; leases exceeding the cap are dropped for recovery rather than blocking sibling leases.Macroscope summarized 11acadd.