kio: rework Producer::poll/wait to a read-only predicate that returns a Mut#1735
Conversation
The Mut-closure poll APIs (Producer::poll/wait, Weak::poll_write/wait) auto-notified consumers whenever the closure touched the value through DerefMut. Since merely draining a queue (e.g. requests.pop() on an empty Vec) trips DerefMut, a pending poll would wake the polling task's own waiter and spin into an infinite loop, an easy-to-hit footgun. Replace them with Producer::poll_drain / Weak::poll_drain, which hand the closure a plain &mut T and never notify. Callers that need to mutate-and-notify still use write(). The only two users, broadcast's poll_requested_track and origin's poll_announced, drain producer-owned queues where notification was unwanted anyway.
Reshape per review: rather than handing the poll closure a plain &mut T, poll_write_when evaluates a read-only predicate over a Ref and, on Poll::Ready, hands back a Mut with the lock still held. The caller decides readiness without touching DerefMut (so an empty-queue poll can't spuriously flag the state modified and wake its own waiter), then mutates atomically through the returned Mut. Keeping the lock held across the upgrade avoids any TOCTOU between the predicate and the mutation. broadcast's poll_requested_track and origin's poll_announced now check "queue non-empty" read-only, then pop/take through the returned Mut.
Restore the descriptive name and add the async sibling so the API matches the poll_xxx()/xxx() convention (poll_closed/closed, etc.): poll_write_when for the poll form, write_when for the async form. Both evaluate a read-only predicate over a Ref and hand back a Mut once it holds.
WalkthroughThe PR replaces the old 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches✨ Simplify code
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. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
rs/moq-net/src/model/origin.rs (1)
882-894: ⚡ Quick winUse
ready!inpoll_announcedpoll plumbing for consistency with project rules.This block manually matches
Pollwhere the guideline expectsready!(...)composition.Suggested refactor
pub fn poll_announced(&mut self, waiter: &kio::Waiter) -> Poll<Option<OriginAnnounce>> { - let res = self.state.poll_write_when(waiter, |state| { + let mut state = match ready!(self.state.poll_write_when(waiter, |state| { if state.pending.is_empty() { Poll::Pending } else { Poll::Ready(()) } - }); - match res { - Poll::Ready(Ok(mut state)) => Poll::Ready(Some(state.take().expect("predicate guaranteed an update"))), - // Closed: discard the Ref so its MutexGuard doesn't escape this call. - Poll::Ready(Err(_)) => Poll::Ready(None), - Poll::Pending => Poll::Pending, - } + })) { + Ok(state) => state, + // Closed: discard the Ref so its MutexGuard doesn't escape this call. + Err(_) => return Poll::Ready(None), + }; + Poll::Ready(Some(state.take().expect("predicate guaranteed an update"))) }As per coding guidelines, in
poll_*plumbing, useready!(...)to collapsePollmatching.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@rs/moq-net/src/model/origin.rs` around lines 882 - 894, In the poll_announced method, replace the manual match on res (which handles Poll::Ready and Poll::Pending cases) with the ready! macro for consistency with project guidelines. Use ready! to unwrap the Poll result, which will automatically propagate Poll::Pending and return the state value for further processing. Then simplify the remaining match statement to only handle the Ok and Err cases from the inner Result, eliminating the explicit Poll::Pending and Poll::Ready wrapping in the match arms.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@rs/moq-net/src/model/origin.rs`:
- Around line 882-894: In the poll_announced method, replace the manual match on
res (which handles Poll::Ready and Poll::Pending cases) with the ready! macro
for consistency with project guidelines. Use ready! to unwrap the Poll result,
which will automatically propagate Poll::Pending and return the state value for
further processing. Then simplify the remaining match statement to only handle
the Ok and Err cases from the inner Result, eliminating the explicit
Poll::Pending and Poll::Ready wrapping in the match arms.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: b1f2b83d-c0f8-4853-9363-45e29f78c61e
📒 Files selected for processing (5)
rs/kio/CHANGELOG.mdrs/kio/src/producer.rsrs/kio/src/weak.rsrs/moq-net/src/model/broadcast.rsrs/moq-net/src/model/origin.rs
💤 Files with no reviewable changes (1)
- rs/kio/src/weak.rs
Producer's read-only predicate poll is now poll() / wait() (was poll_write_when / write_when) so both sides of the channel expose a poll()/wait() pair. They differ in contract by design: Consumer's closure returns the value, while Producer's is a bare Poll<()> predicate that gates handing back a Mut with the lock still held. Also drop `test = false` from kio's [lib] and add a unit test for the new Producer::poll, and tidy origin's poll_announced to use ready! per the poll_* plumbing guideline.
Resolved conflicts from main's kio Producer::poll/wait rework (#1735), which replaced the mutating poll predicate with a read-only one that returns a Mut: - rs/moq-net broadcast.rs / origin.rs / track.rs: ported dev's poll call sites (poll_requested_track, AnnounceConsumer::poll_next, poll_subscription_changed, poll_requested_group) onto the new read-only-predicate API. Mutation now happens through the returned Mut after the predicate gates readiness, which also removes the spurious self-wake footgun #1735 targeted. - js/*: kept dev's refactored APIs (TrackProducer/TrackSubscriber/TrackConsumer split, callable time-unit brands, TrackRequest class) and grafted in main's JSR doc comments where dev lacked them.
Summary
Producer::poll/Producer::waitwere a self-triggering footgun. This reworks them so the closure is a read-only predicate and a satisfied poll hands back aMut.The old versions handed the closure a
&mut Mut<T>and tracked amodifiedflag viaDerefMut. On aPoll::Pending, the poll woke the consumer waiters. The trap: merely reading through a mutable path tripsDerefMut. The canonical case isrequests.pop()on an emptyVec—pop()takes&mut, so an empty (no-op) poll still flags the state modified, wakes a waiter in the very list the poller just registered itself into, re-polls, and spins into an infinite busy loop.Approach
Producer::poll(and its async siblingProducer::wait) now evaluates a read-only predicate over aRef. While the predicate returnsPoll::Pendingnothing is mutated, so it can't spuriously flag the state modified. OnPoll::Readyit upgrades the held lock guard to aMutand hands it back, so the caller mutates atomically — the lock is never dropped between the predicate and the mutation, so there's no TOCTOU even with multiple drainers.Naming: both channel sides now expose a
poll()/wait()pair. They differ in contract by design —Consumer::poll's closure returns the value (Poll<R>), whileProducer::poll's closure is a barePoll<()>predicate that gates handing back aMut.Changes
kio:Producer::poll/Producer::waitreworked to the read-only-predicate-returns-Mutshape.Weak::poll_write/Weak::waithad no users and are removed. The read-onlyConsumer::poll/Consumer::waitare unchanged. Droppedtest = falsefrom the[lib]so unit tests run, and added a test for the newProducer::poll.moq-net: the two callers,BroadcastDynamic::poll_requested_trackandOriginConsumer::poll_announced, now check "queue non-empty" read-only, then pop/take through the returnedMut(the latter tidied to useready!).API note
This is a breaking change to the published
kiocrate's surface (Producer::poll/waitchange signature; theWeakmethods are removed). No other workspace crate used the removed/changed APIs, andmoq-net's public surface is unchanged — the affected helpers are private. Targetingmainper request.Test plan
cargo test -p kio(2 passed, incl. the newpoll_gates_on_predicate_then_writes)cargo test -p kio -p moq-net(346 passed; incl.broadcast::test::requests,requested_unused, origin announce tests)cargo clippy -p kio -p moq-net -p moq-nativecleancargo buildfor kio-dependent crates (moq-net, moq-native, hang, moq-mux, moq-cli, moq-bench)(Written by Claude)