Skip to content

fix(swarm): avoid re-extracting completed outbound substreams - #6427

Open
caniko wants to merge 9 commits into
libp2p:masterfrom
caniko:fix/cannot-extract-twice
Open

caniko wants to merge 9 commits into
libp2p:masterfrom
caniko:fix/cannot-extract-twice

Conversation

@caniko

@caniko caniko commented May 7, 2026 •

Copy link
Copy Markdown

Fix

Select a waiting outbound-substream request before polling the muxer. Completed or absent requests cannot cause an outbound stream to be opened and then discarded; this also avoids extracting a completed request twice.

Regression coverage

Tests cover polling with no waiting request and serving two requested streams while an extra stream is available.

Qualification

Current head: 0c6b55925053f4c5a859448a5828e076f9efabaa. Continuous integration and Interoperability Testing require maintainer approval. The remaining review thread stays open pending swarm qualification. No local tests or evaluations were run during this maintenance pass.

`Connection::poll` selects the next outbound-ready `SubstreamRequested`
via `iter_mut().next()` and calls `.extract()` on it. `extract()` moves
the data out and replaces the variant with `Done`, then relies on
`extracted_waker.wake()` to schedule the cleanup poll that removes the
entry from `requested_substreams`.

The waker is only populated by the `Future::poll` impl when the entry
first observes a `Poll::Pending`. If the entry is extracted before that
first poll ever happens — which can occur when `FuturesUnordered`'s
ordering puts a freshly-pushed entry at the head of `iter_mut()`, or
when the muxer returns `poll_outbound = Ready` on the same loop
iteration as the request was pushed — the waker is `None`, no wake is
scheduled, and the `Done` entry persists.

The next outbound-ready iteration's `iter_mut().next()` lands on that
stale `Done` entry and `extract()` panics.

The intent at this site has always been "find a request waiting for a
substream"; making that explicit with `iter_mut().find(|r| matches!(r,
SubstreamRequested::Waiting { .. }))` removes the dependence on
`FuturesUnordered`'s scheduling. Eager removal of the `Done` entry is
not possible through `FuturesUnordered`'s public API while iterating
it, so the existing wake-driven cleanup stays in place. The number of
un-cleaned `Done` entries is bounded by outstanding outbound requests
and is drained naturally as later events trigger connection polls.

Adds a unit test reproducing the failure shape directly on a
`FuturesUnordered<SubstreamRequested<()>, DeniedUpgrade>>`.
@caniko caniko changed the title swarm: fix panic!("cannot extract twice") in Connection::poll swarm: fix panic!("cannot extract twice") in Connection::poll May 7, 2026
@elenaf9 elenaf9 added the kind/generated This issue might have been automatically generated label May 12, 2026
@elenaf9

elenaf9 commented May 12, 2026 •

Copy link
Copy Markdown
Member

Thank you for the PR @caniko. We appreciate all contributions, including ones that were created with the help of LLMs/ AI-tools.

That said, the sheer amount of text in this PR (descriptions, code comments) makes it very time-consuming to review a change that appears to be just a one-liner.
Could you please:

  • summarize the key change and its motivation in a few lines on a high level
  • reduce code docs to a reasonable level

@caniko

caniko commented May 12, 2026

Copy link
Copy Markdown
Author

Apologies @elenaf9, I usually let these sit and wait as draft, and improve the PR before asking for review. This PR seems to have skipped this step, anyway, it is in the past.

I tried reducing the text, and hope it is good enough for review

@github-actions

Copy link
Copy Markdown

It seems this issue might have been automatically generated. To help us address it effectively, please provide additional details.

We value the use of LLMs for code generation and welcome your contributions but please ensure your submission is of such quality that a maintainer will spend less time reviewing it than implementing it themselves. Verify the code functions correctly and meets our standards. If your change requires tests, kindly include them and ensure they pass.

If no further information is provided, the issue will be automatically closed in 7 days. Thank you for your understanding and for aiding us in maintaining quality contributions!

@github-actions github-actions Bot added the need/author-input Needs input from the original author label May 13, 2026
@elenaf9 elenaf9 removed the need/author-input Needs input from the original author label May 13, 2026
@github-actions

Copy link
Copy Markdown

It seems this issue might have been automatically generated. To help us address it effectively, please provide additional details.

We value the use of LLMs for code generation and welcome your contributions but please ensure your submission is of such quality that a maintainer will spend less time reviewing it than implementing it themselves. Verify the code functions correctly and meets our standards. If your change requires tests, kindly include them and ensure they pass.

If no further information is provided, the issue will be automatically closed in 7 days. Thank you for your understanding and for aiding us in maintaining quality contributions!

@github-actions github-actions Bot added the need/author-input Needs input from the original author label May 14, 2026
@caniko

caniko commented May 14, 2026

Copy link
Copy Markdown
Author

@elenaf9 I think there is something wrong with the bot

@jxs jxs left a comment

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.

Hi, and thanks for this! Left some comments

Comment thread swarm/src/connection.rs Outdated
Comment thread swarm/src/connection.rs Outdated
@caniko caniko changed the title swarm: fix panic!("cannot extract twice") in Connection::poll fix(swarm): avoid re-extracting completed outbound substreams May 25, 2026
@caniko

caniko commented May 25, 2026 •

Copy link
Copy Markdown
Author

Updated per review: extract() now returns Option, the connection path uses that to skip stale Done entries, and the regression coverage now goes through Connection::poll instead of directly manipulating SubstreamRequested. I also renamed the PR title to satisfy the semantic-title check.

Validation run locally with an ad hoc Nix Rust shell because the downstream project flake is currently blocked by a stale rs-detritus input:

  • cargo test -p libp2p-swarm connection_poll_skips_done_substream_requested_entries
  • cargo test -p libp2p-swarm

Comment thread swarm/src/connection.rs Outdated
@mergify

mergify Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts. Could you please resolve them @caniko? 🙏

Comment thread swarm/src/connection.rs Outdated
Comment on lines +414 to +420
match muxing.poll_outbound_unpin(cx)? {
Poll::Pending => {}
Poll::Ready(substream) => {
if let Some((user_data, timeout, upgrade)) = requested_substreams
.iter_mut()
.find_map(SubstreamRequested::extract)
{

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.

sorry I just noticed that If the muxer returns Poll::Ready(substream) but all entries are Done (so find_map returns None), the stream is silently dropped

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch, fixed in 09f0491. The continue is now inside the if let Some block so we only loop back when we actually consumed the stream. When no Waiting entry exists, we fall through to the rest of poll logic.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Nice catch! Fixed now

If poll_outbound_unpin returns Ready(substream) but all
requested_substreams entries are Done, do not unconditionally
continue — the substream had no matching request and the
loop would busy-spin. Fall through instead.
@caniko

caniko commented Aug 29, 2026

Copy link
Copy Markdown
Author

@jxs 09f0491 only stops the busy-spin: continue is inside the if let Some. If poll_outbound is Ready and find_map(extract) is None, the substream is still dropped.

Would you prefer restoring the old Waiting precheck so we do not call poll_outbound without a request, or is the current fall-through intended? I can also rebase the branch once that direction is clear.

Published through GitHub’s verified commit API during the hosted-only PR maintenance pass.
@caniko

caniko commented Oct 4, 2026

Copy link
Copy Markdown
Author

fix(swarm): open outbound streams only for waiting requests in 0c6b55925053f4c5a859448a5828e076f9efabaa. Hosted CI is the validation gate for this maintenance pass; no evaluation or tests are run locally.

@caniko

caniko commented Oct 4, 2026

Copy link
Copy Markdown
Author

The outbound-stream ownership repair and regressions are at 0c6b55925053f4c5a859448a5828e076f9efabaa. Current-head code qualification is awaiting maintainer approval: Continuous integration and Interoperability Testing both conclude action_required. The successful Semantic PR/Summary checks do not establish swarm-test acceptance. My attempt to approve the CI run returned Must have admin rights to Repository (403), and the fork CI carrier has not discovered any workflows. Could a maintainer approve the two upstream runs so the regressions can be qualified? No local tests or evaluations were run; the remaining stream-ownership thread is kept open pending that evidence.

This branch has not been deployed

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

Labels

kind/generated This issue might have been automatically generated need/author-input Needs input from the original author

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants