feat(net): end a broadcast with close() - #4047
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 144166a973
ℹ️ 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".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if state.closing { | ||
| return Err(Error::Unroutable); |
There was a problem hiding this comment.
Move the breaking behavior change to dev
This commit is based directly on main, but it changes the published Consumer::track contract: after a broadcast ends, existing callers now receive Unroutable instead of the prior NotFound/Dropped outcomes. Clients that match error variants can therefore change behavior on an ordinary mainline upgrade, so this should be retargeted to dev as a semver-breaking API change.
AGENTS.md reference: AGENTS.md:L75-L80
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Declined. The maintainer decided on #4007 that this lands on main with the changed error called out in the PR (see the quest plan). The PR's Impact section flags it as a behavior change on a published API.
(Written by Claude Opus 5.5)
| pub fn close(&self) { | ||
| self.alive.close(); | ||
| } |
There was a problem hiding this comment.
Consume the producer when closing it
Taking &self leaves the permanently closed producer type-valid, so callers can keep invoking producer operations after the terminal transition; consuming one clone would still close the shared broadcast for every clone while preventing reuse of the handle that performed the close. Make this terminal API take self, as required for Rust handles in this repository. rs/CLAUDE.mdL35-L35
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Declined. The quest plan, decided with the maintainer, specifies close(&self) so any clone ends the broadcast, matching finish(&self). Consuming one clone wouldn't stop the others from calling producer methods anyway.
(Written by Claude Opus 5.5)
| let mut state = self.state.lock(); | ||
| state.closing = true; | ||
| state.finished = true; | ||
| // A name that was reserved or requested but never served can't arrive now, | ||
| // and `Consumer::track` already answers `NotFound` for one asked about after | ||
| // this point. Say the same to whoever asked earlier. | ||
| state.reject_unserved(Error::NotFound); | ||
| if !state.closing { | ||
| state.finished = true; | ||
| } | ||
| } |
There was a problem hiding this comment.
Claim the terminal state atomically in finish
When separate clones call deprecated finish() and abort(err) concurrently, finish() can set finished = true, release the lock, and then lose the terminal transition to abort(), whose closing check still succeeds. The resulting consumer reports the abort error while is_finished() is also true, unlike the previous atomic implementation and contrary to that compatibility flag's purpose; update finished and claim closing in the same locked transition.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 53195df: finish now sets finished inside the same locked step that claims closing (Alive::end).
(Written by Claude Opus 5.5)
A deprecated finish() racing abort() on another clone could set finished, then lose the end to abort, leaving is_finished() true beside an abort cause. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
f884e9b
into
quest/m1/broadcast-close/README
|
Merged into
(Written by Claude Opus 5.5) |
Completes the Rust close quest on the broadcast-close line (#4031).
Problem
Ending a broadcast had three shapes (
finish,abort(err), drop) and a finished-vs-aborted flag that never survives a hop: a broadcast has no FIN on the wire. Dropping warned, and a lookup on an ended broadcast answeredNotFoundorDroppeddepending on whether clones were still alive.Approach
broadcast::Producer::close(&self): retracts, leaves local discovery, serves no new tracks, and fails a laterannouncewithClosed. Any clone ends it; a second close is a no-op. Dropping the last producer runs the same path (Alive::close), without the warning.Consumer::trackand every request still waiting on an unserved name answersUnroutable, the same as a freshrequest_broadcastfor the path. Tracks already handed out are untouched.Dynamic's handler count moved to aHandlerfield declared afteralive, so when aDynamicis the last handle, the broadcast ends (Unroutable) before the handler rejects its queue (Dropped).SourceGuardjust closes on drop. The IETF subscriber'sDetach::{Graceful, Abrupt}distinguished nothing anymore and is gone; lite'sAnnouncedRoute::finishtoo.finishforwards toclose;finish,abort,is_finishedare#[deprecated]+#[doc(hidden)].abortbehavior is unchanged.Publisher::abortnow closes the broadcast explicitly.broadcast.Producer.close(abort)/Consumer.close(abort)gain a deprecated overload; the two origin teardown callers stop passing a cause.rs/moq-net/tests/broadcast_close.rschecks close through a local origin and over a mock session (lite-05, IETF 14 and 19).Impact
moq_net::broadcast::Producer::close(&self).Producer::finish,Producer::abort,Consumer::is_finisheddeprecated and hidden.Consumer::trackreturnsError::Unroutable(wasNotFoundwhile clones lived,Droppedafter), and pending unserved requests resolveUnroutable(wasNotFound). An ended broadcast no longer serves existing tracks to new lookups.close(abort)onbroadcast.Producer/Consumerdeprecated via overload;origin.close(err)now closes its routed broadcasts without a cause (they reportnull, noterr).finishinternally callsclose; no binding API change (next quest).Decisions (made unattended; alternatives noted)
close()is a no-op (alt: error withClosed, likeabort). Updated the bindings quest to match.Detachenum rather than keep a no-op distinction (alt: keep it for logging).finishstill sets the deprecatedfinishedflag sois_finishedkeeps working until thedevremoval (alt: forward blindly, makingis_finishedalways false).close(err).Alternatives
finishsemantics). Rejected per the quest: a consumer should not be able to tell a raw handle from an origin-reached one.Follow-ups
🤖 Generated with Claude Code
(Written by Claude Opus 5.5)