Skip to content

fix(sse): saturate exponential reconnect backoff to avoid overflow panic - #1231

Open
ump45nose wants to merge 2 commits into
modelcontextprotocol:mainfrom
ump45nose:contrib/631d546176b5
Open

fix(sse): saturate exponential reconnect backoff to avoid overflow panic#1231
ump45nose wants to merge 2 commits into
modelcontextprotocol:mainfrom
ump45nose:contrib/631d546176b5

Conversation

@ump45nose

Copy link
Copy Markdown

Summary

fix(sse): saturate exponential reconnect backoff to avoid overflow panic

Verification

cargo test -p rmcp --features client-side-sse --lib client_side_sse (16 passed); cargo clippy -p rmcp --features client-side-sse --lib (no new warnings); cargo fmt --check (clean)

Related to #1198

AI assistance disclosure: AI was used to discover this opportunity and draft the change or text. The submission was checked against the prepared artifact and recorded verification evidence.

ExponentialBackoff::retry computed the reconnect multiplier with
2u32.pow(current_times). With max_times unset, current_times can reach
the bit width, panicking in debug builds and wrapping to a zero delay in
release builds for long-lived SSE clients. Use saturating_pow and
Duration::saturating_mul so the delay stays monotonic and panic-free.
@ump45nose
ump45nose requested a review from a team as a code owner August 31, 2026 09:23
@github-actions github-actions Bot added T-core Core library changes T-transport Transport layer changes labels Aug 31, 2026
Comment on lines +234 to +235
let multiplier = 2u32.saturating_pow(current_times as u32);
Some(self.base_duration.saturating_mul(multiplier))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once the multiplier reaches its limit, the delay can be decades long. The caller passes it directly to tokio::time::sleep, so the stream neither reconnects nor terminates.

Saturating the multiplier alone can still yield decades-long sleeps once
current_times reaches the bit width, pinning the stream in
tokio::time::sleep without reconnecting or terminating. Add an optional
max_delay (default 30s) that clamps the computed delay, keeping the
backoff monotonic and panic-free while guaranteeing the client retries.
@ump45nose

Copy link
Copy Markdown
Author

Thanks for the catch — you're right that a saturating multiplier alone leaves the delay unbounded, and at the saturated value tokio::time::sleep would park the stream for decades without reconnecting or terminating.

Pushed a follow-up that adds an optional ExponentialBackoff::max_delay (default DEFAULT_MAX_DELAY = 30s). The computed delay is now clamped to that ceiling, so the backoff stays monotonic and panic-free while guaranteeing the client actually retries. max_delay: None restores the previous unbounded (but now saturating) behavior for callers that want it.

Added a regression test (exponential_backoff_caps_delay_at_max_delay) asserting the delay grows monotonically, never exceeds the cap, and pins at max_delay past the saturation point.

AI assistance disclosure: AI was used to discover this opportunity and draft the change or text. The submission was checked against the prepared artifact and recorded verification evidence.

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

Labels

T-core Core library changes T-transport Transport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants