feat(xmtp_api_d14n): XIP-83 bidi half-close + live integration tests#3789
Conversation
|
Claude finished @tylerhawkes's task —— View job Code Review CompletedReview Date: June 29, 2026 at 21:59 UTC Based on my analysis of the XIP-83 bidi half-close implementation, I've identified the following areas that warrant attention: Outstanding Critical IssuesThe three unresolved review comments from macroscopeapp identify real correctness concerns that should be addressed:
Code Quality ObservationsPositive aspects:
Minor improvements:
Test CoverageThe codecov report shows 91.52% patch coverage with 15 lines missing. Given the nature of this feature (actor-based concurrency with multiple exit paths), this is reasonable. The four integration tests provide strong end-to-end validation. Performance & SecurityNo performance or security concerns identified. The bounded buffers and backpressure mechanisms are sound. Summary: The implementation is well-structured and thoroughly tested. The three unresolved review comments represent real edge cases that should be addressed before merge, particularly the race condition in |
ApprovabilityVerdict: Needs human review 2 blocking correctness issues found. This PR adds new XIP-83 bidi half-close functionality with significant concurrent state management and stream lifecycle changes. The new runtime behavior and unresolved review comments about potential concurrency issues warrant human review. You can customize Macroscope's approvability policy. Learn more. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3789 +/- ##
========================================
Coverage 84.54% 84.54%
========================================
Files 410 410
Lines 60716 60843 +127
========================================
+ Hits 51331 51440 +109
- Misses 9385 9403 +18 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
cec5652 to
b7f1207
Compare
b7f1207 to
4856bda
Compare
4856bda to
2b4a310
Compare
2b4a310 to
9a92118
Compare
Builds on the merged BidiConnection actor (#3772): - TrackedStatsClient forwards XmtpMlsBidiStreams so the feature-switched test client can open a BidiConnection. - BidiConnection::finish() half-closes the request half for bounded-sync. - Live v3 integration tests: handshake+welcome, catch-up/marker/live ordering, history_only, and bounded-sync half-close.
9a92118 to
5d2c9c4
Compare
…kend (#3804) Prerequisite #3789 has merged; this now targets `main` directly. Generalizes the XIP-83 bidirectional subscription connection over a `BidiBinding` trait so the backend-agnostic control core lives once, and adds the d14n backend. ## What this does - **`Connection<B: BidiBinding>`** (`queries/bidi.rs`) — the control core (probe + timeout, the non-blocking select loop, the give-up cap, `finish()`/half-close + best-effort flush, and ownership teardown) now lives once, generic over a backend. - **`BidiBinding`** — each backend supplies only its wire vocabulary: request/response/mutate/message types, frame construction, and a `handle()` that classifies an inbound response into an `Inbound` instruction. - **v3 binding** (`queries/v3/connection.rs`) — thin: `mls_v1` frames, messages surfaced as raw proto. Its public API (`BidiConnection`, `BidiEvent`) is preserved via type aliases, so the #3789 tests are unchanged. - **d14n binding** (`queries/d14n/connection.rs`) — a stateless, pure **extractor**: it demultiplexes each delivered `OriginatorEnvelope` batch by topic kind and runs the matching extractor into the unified `xmtp_proto::types::{GroupMessage, WelcomeMessage}`, surfacing them in wire order. Symmetric with the v3 binding — no reordering happens at the transport. - **`TrackedStatsClient::inner()`** — lets the d14n test reach the concrete `D14nClient` through the stats wrapper. ## Ordering The d14n binding deliberately does **not** reorder envelopes (no icebox / cursor pipeline). Rationale: - Per **XIP-49 §1.3**, the broadcast network is *not required* to totally order. The payloads that do need strict ordering — commit and identity messages — are chain-anchored to a single fixed originator (`Originators::MLS_COMMITS = 0`), so MLS's epoch chain is the real dependency and gates processing natively: an out-of-order application message just waits for its epoch. - **XIP-49 §3.3.5** makes cross-originator causal ordering explicitly optional ("clients are free to define an ordering strategy… for additional trustlessness… at the cost of additional complexity"). A single per-process transport is the wrong layer for that. If a consumer wants trustless causal ordering it belongs as a durable *client-level* icebox — the server-streaming path's `OrderedStream` is left untouched for exactly that use. This keeps the d14n binding a dumb envelope shoveler, symmetric with v3.
What
Builds on the merged XIP-83
BidiConnectionactor (#3772). Adds the bounded-sync half-close API and the first live integration tests that exercise a realBidiConnectionagainst a running node, plus the small forward that lets the standard test client open one.Changes
TrackedStatsClientforwardsXmtpMlsBidiStreams(xmtp_api_d14n/src/queries/api_stats.rs, native-only). The stats wrapper forwarded every client trait except bidi, so the feature-switchedTestClient(TrackedStatsClient<V3Client>) could not open aBidiConnection. One delegating impl, no per-call stat (the bidi stream is opened once and mutated in place, not counted per RPC).BidiConnection::finish()— half-close / bounded sync (XIP-83 server req 9 / client req 4). A newCommand::Finishmakes the actor drop its outbound (request) half so the stream half-closes; the server finishes the in-flight catch-up wave, emits itsTopicsLive/CatchUpCompletemarkers, and closes its side, so the consumer drainsnext()toNone. Afterfinish(),mutate/probereturnClosed. Opened withMutate::history_only, this is the bounded catch-up ("sync") flow over the same connection. The response→event translation is factored into a sharedemit_response_eventsso the live loop and the post-finish drain deliver identical events.Live v3 integration tests (
xmtp_mls/src/subscriptions/bidi_tests.rs). v3-only and native-only (the bidi transport is implemented for the v3 client and needs full-duplex HTTP/2;TestClientis only bidi-capable under the v3 config). They open a realBidiConnectionagainst whichever backend the test feature switch selects and derive every assertion from the stream itself — using each frame'sis_commitflag to count application messages independently of the MLS commits a membership change produces, rather than a side query:bidi_connection_delivers_live_welcome_over_the_wire— handshake, live welcome after a membership change, ping/pong probe.bidi_catch_up_precedes_live_marker_then_streams_live— the happy path in one test: pre-subscription history is caught up strictly before theTopicsLivemarker; messages published while the stream starts arrive exactly once on either side of the marker; post-marker sends stream live and are strictly newer than every catch-up cursor;CatchUpCompleteechoes themutate_id.bidi_history_only_catches_up_then_delivers_nothing_live—history_onlycatches up and emits the markers but registers nothing for live delivery.bidi_history_only_half_close_drains_then_server_closes— bounded sync:history_only+finish(), the server closes after the wave, andmutatethen returnsClosed.Testing
All four integration tests pass against a local
node-go:maincarrying the bidiSubscribesupport. The 13BidiConnectionactor unit tests still pass after the refactor.just lint-rustis clean; the v3-only test module was additionally linted withcargo clippy -p xmtp_mls --tests(the workspace lint runs--all-features, which enablesd14nand so skips this module).Note
Add XIP-83 bidi half-close support and live integration tests to
BidiConnectionfinish()method toBidiConnectionthat half-closes the outbound stream: the actor stops accepting new commands, flushes any pending frames best-effort, drops the outbound channel, then drains inbound until the server closes while continuing to emit events.Command::Finishin the actor loop and anAtomicBoollatch somutate()andprobe()returnClosedimmediately afterfinish()is delivered.emit_response_eventsanddrain_after_finishas helpers in connection.rs to cleanly separate teardown logic from the main actor loop.history_only+finish.XmtpMlsBidiStreamsforTrackedStatsClienton native targets, forwardingsubscribe_bidito the inner client.Macroscope summarized 5d2c9c4.