Skip to content

fix(tokio): keep a deferred FIN when a send stream drops mid-watch - #5156

Merged
kixelated merged 1 commit into
mainfrom
fix/tokio-closing-drop-keeps-fin
Oct 10, 2026
Merged

kixelated merged 1 commit into
mainfrom
fix/tokio-closing-drop-keeps-fin

Conversation

@kixelated

Copy link
Copy Markdown
Collaborator

Codex found this on the release backport #5133. main has the same code, so this PR fixes it here first.

The bug

Since #4658, moq-tokio's wrapped SendStream::poll_closed starts the real closed() watch on an idle, unfinished stream (SendState::Closing), and that future owns the stream. If finish() is called while the watch holds the stream, the FIN is only recorded as deferred and the watch is interrupted. If the stream is then dropped before another poll, Drop's Closing branch rescued only a deferred reset. It dropped the future, so the backend did its default drop reset instead of the FIN that finish() had already reported as successful.

Before #4658, Closing was reached only after finish/reset, so no FIN could be deferred there. moq-net's own Writer resets on drop unless closed, so today's moq-net callers do not hit this. It is still a contract bug in the adapter.

Fix

The Closing drop branch now reclaims the interrupted watch and applies a deferred finish as well as a deferred reset, the same way the Writing branch already does.

Public API and wire impact

None.

Tests

  • New a_deferred_finish_survives_a_drop_mid_watch (paused time). It fails without the fix ("the deferred FIN was lost") and passes with it.
  • cargo nextest run -p moq-tokio: 451 passed.
  • just check: everything else passes, but moq-cli publish::tests::ts_passthrough_crosses_a_relay_through_a_flagged_jump fails (moq-transport-14, "both copies crossed"). It is a wall-clock test, and it fails the same way on unmodified main when its sibling tests run alongside it. It passes when run alone.

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

An idle closed watch owns the stream, so finish() only records the FIN.
Dropping the stream before the next poll discarded the watch and let the
backend reset instead. Reclaim the interrupted watch and apply a deferred
finish as well as a deferred reset, like the in-flight write path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 10, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 40 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: f7f8741d-b39d-4264-825d-7648a984885c

📥 Commits

Reviewing files that changed from the base of the PR and between 31a54a8 and 8a91d0d.


📒 Files selected for processing (1)
  • rs/moq-tokio/src/transport.rs


  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kixelated

Copy link
Copy Markdown
Collaborator Author

Grok review of 8a91d0d

Small, targeted fix: the Closing drop branch now reclaims the interrupted closed() watch and applies a deferred FIN, mirroring the Writing branch. The logic checks out: reset takes priority over finish, and mem::take clears the flag. The new paused-time test covers the exact scenario.

No blocking issues.

Non-blocking:

  • transport.rs Closing branch: when there is no Tokio runtime (Handle::try_current() fails), a deferred FIN is still silently lost and the backend does its drop reset. That matches the existing reset behavior, but since finish() already returned Ok, a tracing::warn! there (or a comment noting the limitation) would make it visible.
  • let _ = stream.finish(); drops the error silently. A debug log would help when the salvage finish fails.
  • The test asserts finished after 10 yield_nows. That's fine with the fake, but if FakeSend's watch ever needs a timer, the test may need tokio::time::advance too.
  • CI is still pending on this head.

Verdict: MERGE (once CI is green)

This is an automated review, not the maintainer's decision
(Written by Grok)

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-10T00:56:34.310812Z 8a91d0d PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@kixelated

Copy link
Copy Markdown
Collaborator Author

Merge summary for 8a91d0d

The Closing drop branch now reclaims the interrupted closed() watch and applies a deferred FIN as well as a deferred reset, mirroring the Writing branch. The new paused-time test fails without the fix. No public API or wire impact.

Reviews on this head: Codex finished with a thumbs-up and no findings; Grok had no blockers. CI is green.

On Grok's non-blocking notes, I'm leaving the code as is:

  • No Tokio runtime: the shared comment above the Writing branch already says "Without a runtime the default drop stands", and that applies to both salvage branches. A drop with no runtime can't run the async salvage at all, so this is a documented limitation, not a silent new loss.
  • let _ = stream.finish();: same as the existing Writing branch. The stream is being dropped, so there's no caller to report a failed salvage finish to. If we want visibility, a debug log should go into both branches together in a follow-up, not just this one.

The release backport #5164 cherry-picks this fix and is handled separately.

Enabling squash auto-merge pinned to this head.

(Written by Claude Opus 5.5)

@kixelated
kixelated merged commit a5b17e3 into main Oct 10, 2026
7 checks passed
@kixelated
kixelated deleted the fix/tokio-closing-drop-keeps-fin branch October 10, 2026 12:40
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.

1 participant