Skip to content

fix(xmtp_mls): stabilize should_reconnect flaky test in d14n suite#3446

Open
xmtp-coder-agent wants to merge 6 commits into
xmtp:mainfrom
xmtp-coder-agent:fix/issue-3438
Open

fix(xmtp_mls): stabilize should_reconnect flaky test in d14n suite#3446
xmtp-coder-agent wants to merge 6 commits into
xmtp:mainfrom
xmtp-coder-agent:fix/issue-3438

Conversation

@xmtp-coder-agent

@xmtp-coder-agent xmtp-coder-agent commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

Resolves #3438

Summary

xmtp_mls::client::tests::should_reconnect exhausts its retry budget in the --features d14n nextest suite with a Status::Cancelled / "connection closed" error on the second stream_conversations call — the one that verifies reconnection after the toxiproxy blackhole is lifted.

Root cause

  1. During the blackhole, HTTP/2 keep-alive pings time out and tonic marks the Channel as disconnected.
  2. The test removes the toxics and asserts the old stream is done.
  3. The test then opens a new stream. tonic::transport::Channel reconnects lazily on the first RPC after a transient failure — and that first RPC can observe hyper::Error(Canceled, "connection closed")tonic::Status::Cancelled if it fires while the channel is still transitioning out of the failure state.

Why d14n flakes more than v3

The ToxicOnly d14n test client is D14nClient<ReadWriteClient<ToxicXmtpdClient, ToxicGatewayClient>>. ReadWriteClient holds two independent tonic Channels (one per backend) and for_each applies/removes toxics on both. With two channels to recover in lock-step, the reconnect race is roughly twice as likely to land inside the "Channel still transitioning" window. The v3 toxic client has one proxy / one channel.

Fix

Wrap the final stream_conversations call in the existing xmtp_common::retry_async! macro with an ExponentialBackoff strategy (5 retries, 100ms initial backoff). GrpcError is already universally retryable, so the retry mechanism correctly handles transient transport errors. Non-transport errors short-circuit immediately via RetryableError, so the test still fails loudly on genuine failures.

Why retry_async! and not a longer sleep

A longer sleep would need to be sized for the worst case, increasing test duration on every run. retry_async! already exists, already honours RetryableError, and only waits as long as the channel actually needs.

Scope

Test-only change. No production code is touched:

  • ❌ No changes to stream_conversations
  • ❌ No changes to tonic/hyper keep-alive settings
  • ❌ No changes to for_each_proxy or ReadWriteClient

The earlier assertions that verify the stream errored out during the blackhole and the assert_stream_done! check are unchanged. Same test name, same final assertions on the new stream's first item.

Test plan

  • cargo clippy -p xmtp_mls --features test-utils,d14n --tests --no-deps -- -Dwarnings
  • cargo clippy -p xmtp_mls --features test-utils --tests --no-deps -- -Dwarnings (v3 build)
  • cargo fmt --check
  • just test d14n -E 'test(should_reconnect)' — run the single test in the failing suite (requires backend; please run in CI)
  • just test v3 -E 'test(should_reconnect)' — confirm no regression in v3
  • Run the d14n test in a loop (≥10 iterations) to confirm stability

🤖 Generated with Claude Code

Note

Retry subscription setup in subscribe_group_messages and subscribe_welcome_messages to fix flaky test

Wraps the initial subscription API calls in mls.rs with retry_async! using the existing retry_strategy, so transient transport errors during subscription setup are retried rather than immediately failing. This stabilizes the should_reconnect test in the d14n suite.

Macroscope summarized b7bf1de.

Wrap the post-blackhole `stream_conversations` call in a bounded
`retry_async!` with exponential backoff. After toxics are removed,
tonic's `Channel` reconnects lazily on the next RPC, and the first
post-reconnect call can race the reconnect and observe a
`Status::Cancelled` / "connection closed" transport error. d14n is
more exposed to this because `ReadWriteClient` holds two independent
channels (xmtpd read + gateway write) that both need to recover
before the new stream can be established.

Non-transport errors short-circuit immediately via `RetryableError`,
so the test still fails loudly on genuine failures. Earlier
assertions (stream errored during blackhole, `assert_stream_done!`)
are unchanged. Test-only change — no production code modified.

Resolves xmtp#3438

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@codecov

codecov Bot commented Apr 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.16%. Comparing base (701cc12) to head (b7bf1de).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3446      +/-   ##
==========================================
+ Coverage   85.12%   85.16%   +0.04%     
==========================================
  Files         411      412       +1     
  Lines       63290    64141     +851     
==========================================
+ Hits        53873    54626     +753     
- Misses       9417     9515      +98     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@neekolas
neekolas marked this pull request as ready for review April 10, 2026 16:09
@neekolas
neekolas requested a review from a team as a code owner April 10, 2026 16:09
Comment thread crates/xmtp_mls/src/client.rs Outdated
@neekolas
neekolas marked this pull request as draft April 10, 2026 16:10
Address review feedback on xmtp#3446: rather than wrapping the test in a
retry loop (which hides the reconnect race from user code), wrap the
production `ApiClientWrapper::subscribe_welcome_messages` call in
`retry_async!(self.retry_strategy, ...)`.

After a network blip the underlying tonic `Channel` reconnects
lazily on the next RPC, and the first post-reconnect call can
observe `Status::Cancelled` / "connection closed" while the channel
is still transitioning out of its transient-failure state. This
matches the existing retry pattern used by the unary RPCs in the
same wrapper (`upload_key_package`, `fetch_key_packages`, etc.) and
means user code gets transparent resilience to transient transport
errors on stream subscription setup, without any special-case
handling.

The `should_reconnect` test is restored to its original form — no
test-side retry workaround, so the test still verifies that a
single `stream_conversations` call succeeds after toxics are
removed, preserving the spirit of the test.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@tylerhawkes

Copy link
Copy Markdown
Contributor

Even though the test-workspace workflow passed, there were still retries for the should_reconnect test. It is not fixed.

Comment thread crates/xmtp_api/src/mls.rs Outdated
Apply the same retry wrapping to subscribe_group_messages and
subscribe_group_messages_with_cursors that already protected
subscribe_welcome_messages, and trim the explanatory comment down
to a single reference to issue xmtp#3438.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@insipx

insipx commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

@xmtp-coder-agent pull in latest and resolve conflicts

@insipx

insipx commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

@xmtp-coder-agent you didn't resolve all the conflicts yet

# Conflicts:
#	crates/xmtp_api/src/mls.rs
@neekolas

Copy link
Copy Markdown
Contributor

Resolved and pushed (b7bf1dee9). My earlier push had merged a stale main; since then 81 more commits landed (including the #3828 transport-reconnect / BidiTransport rework), which is what re-introduced the conflict.

The only real conflict was in crates/xmtp_api/src/mls.rs: main added #[xmtp_common::rpc_span] to the subscribe_* methods, right where this PR adds the retry wrapping. Resolved by keeping both — every subscribe_* method now has the rpc_span attribute and the retry_async! wrap. GitHub now reports the PR as mergeable.

One thing worth a look on your end: #3828 adds transparent reconnect at the d14n transport layer. If that already covers the post-reconnect first-RPC failure this PR targets, the wrapper-level retry here may be redundant — happy to close in favor of it, or keep it as defense-in-depth. Your call.

@neekolas

Copy link
Copy Markdown
Contributor

The failing check (test-browser-sdk / shard 4DebugInformation.test.ts > should return network API statistics) was a flake, not a regression from this PR — it passed on re-run of the same commit (b7bf1dee9) with no code change, and the whole run is now green.

Root cause (unrelated to this PR's change):

  • The test calls clearAllStatistics() and then immediately asserts apiStats2.queryGroupMessages === 0n.
  • queryGroupMessages is incremented once per group sync_with_conn() / receive(). On a freshly-created client, the DeviceSync worker spawns automatically at client build and its sync_init → get_sync_group creates + syncs the sync group on first run (crates/xmtp_mls/src/worker/device_sync/mod.rs:368), issuing one query_group_messages.
  • When that background sync races in between clearAllStatistics() and the stats read, the counter reads 1n instead of 0n. It's the same class of background-worker-vs-exact-count race the test already documents for the identity stats (lines 21–28), which were softened to lower-bound assertions for exactly this reason.

This PR only changes crates/xmtp_api/src/mls.rs (wrapping the three subscribe_* methods in retry_async!). Those methods increment subscribeMessages / subscribeWelcomes, never queryGroupMessages, so this change cannot affect the failing assertion.

No code change needed here to make CI pass. If maintainers want to de-flake the test itself, the fix would mirror the identity-stats treatment — assert a bound / await worker quiescence after clearAllStatistics() rather than an exact 0n — but that's a separate change to the newly-migrated browser-sdk suite, out of scope for this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Flaky CI Failure: xmtp_mls::client::tests::should_reconnect fails all retries in d14n suite

4 participants