Repository navigation
feat!: generate async wrappers behind default-off async features - #7607
Conversation
Quest-Claim: 7317d8676efbc10f
Keep poll methods canonical and enable the wrappers in their existing Rust consumers. Leave the overlapping origin conversion and remaining libraries in the quest. Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Replace 48 hand-written `kio::wait(|waiter| self.poll_x(waiter)).await` wrappers in moq-net with `#[cfg_attr(feature = "async", kio::wrapper)]` on their polls, merging each wrapper's docs into its poll so one doc reads for both forms. `group::Producer::ready` was compiled without `async`; it is now generated behind the feature like the rest. Gate `auth::until`, `RequestGrant::guard`, and `util::sendable` (their only remaining caller) behind `async`, since only the async IETF session uses them, and drop `--allow until` from `just rs coroutines`. Point moq-json's module docs at `Producer::poll_refresh`, which the poll-only doc build in `just rs coroutines` can resolve. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The origin's per-broadcast front driver was the last pair of coroutines (`run_front`, `serve_front`) moq-net compiled without its `async` feature. It is now a hand-written `kio::Task`: `FrontServe` holds what the coroutine's locals held and turns its one `kio::wait` into `poll_step`, and `FrontServeRun` adds the lazy start and the in-flight phase. Each wait still gets a fresh waiter, as `kio::wait` gave it, so a finished wait's registrations cannot wake the front spuriously. `TasksWeak::push` takes a poll task, and the submission queue holds boxed poll tasks, so the front and the socket pump queue without a future adapter; `Tasks::push` adapts its futures on submission. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in feature
Each poll-based sibling gains a default-off `async` feature, its one-poll
wrappers become `#[kio::wrapper]` on their polls, and its poll-only build
joins `just rs coroutines`.
- moq-room: `Room::next`, `chat::Publisher::expire`, and
`chat::Subscriber::recv` are generated; `chat::Subscriber::subscribe` is
gated.
- moq-stats: the readers gain public `poll_next` with generated `next`.
`Consumer::{totals, presence, prefix, group}` return their reader at once
and finish the subscription handshake on its first read, as the
aggregate consumer already does. `Publisher::poll_run` drives the publish
loop; `run(self)` stays a hand-written by-value wrapper behind `async`.
- moq-crypto: the `sign` and `e2ee` wrappers are generated.
- moq-auth: the lease wrappers are generated; `poll_revalidate` is
`poll_revalidate_requested`, after its wrapper. `reported` is gated, and
`client` and `serve` enable `async`.
The coroutine scan takes a manifest path for moq-auth, whose name its
published fixture shares. moq-relay enables the features it awaits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pers The runtime glue and bindings are always async, so their one-poll wrappers are an unconditional `#[kio::wrapper]` on the poll, which carries the merged docs, and their kio dependency enables `async`. moq-uring's `poll_read_chunk` and `poll_received_reset` become public beside their generated wrappers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… moq-mux, and moq-hls hang, moq-mux, and moq-hls gain a default-off `async` feature. Their hand-written `kio::wait` wrappers become `#[cfg_attr(feature = "async", kio::wrapper)]` on the polls, and the async functions with logic of their own compile only with the feature. moq-mux's TS follower subscribes to a catalog through a crate-private poll type instead of a boxed `async` block, so its poll-only build keeps it. moq-hls gates its import and export gateways behind `async`, which `server` enables. All three join `just rs coroutines`, and their consumers enable the feature where they await. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
moq-net builds its poll model alone unless a dependent enables `async`: moq-tokio's `net`, moq-uring, moq-archive, and moq-mux's `async` forward it, and test targets that await moq-net enable it through a dev-dependency. Every coroutine scan now passes without an allowance, so drop `--allow`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Server::accept_request now waits on a hand-written Accept machine over the transport's ALPN: moq-transport's SETUP (ietf::SetupAccept, now a poll), a lite Setup Stream (lite::SetupAccept), or the negotiated bidi SETUP with its hand-off reply. Handshake::ok's negotiated path flushes its server SETUP through a NegotiatedReply poll. Writer::closing splits close into a FIN and a Closing poll that, like close, never resets on drop. The async accept_setup helpers and Stream::accept remain only as test futures. accept_request keeps its signature, error codes, close on refusal, accept budget, and what a dropped handshake releases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The merge from main added a hand-written wrapper; generate it like the rest, and restore the blank lines the merge dropped between migration notes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only the boxed transport, which needs `async`, sizes a stream's window, so the poll-only lint failed on main with `window` unused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Futures not yet written as polls go through a transitional future_task adapter. Records the maintainer's decision to convert the remaining async bodies here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…elpers for tests 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>
…hine Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Design change: per the maintainer's 2026-11-05 decision (option B), this PR now also converts every remaining multi-await
The review restarts with (Written by Claude Opus 5.5) |
kio's Timer lints as a large enum variant only in builds without its sim clock, and a moq-net test from #7690 calls an async-only read; both fail on main too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ports #7678's route listing and Following changes into the IETF namespace machines and moq-hls's follow machines, takes its Activity::poll_close, and links Following::poll_available so poll-only docs resolve. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decided 2026-11-05 (maintainer): leave moq-hls's HTTP import driver and routes async as HTTP edge glue, like moq-auth's client and server. That was the epic's last open item, so the epic is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Reviewed the async-to-poll conversion against the async fn bodies it replaces, with the IETF data plane and the origin front read from git diff --merge-base origin/main because the exported diff renders publisher.rs and subscriber.rs as binary.
- Generated wrappers —
#[cfg_attr(feature = "async", kio::wrapper)]on the one-poll polls (48 in moq-net, plus moq-json, hang, moq-mux, moq-room, moq-stats, moq-crypto, moq-auth) and unconditional#[kio::wrapper]on the always-async edges; each removedasync fnmaps to the same poll and docs, and the renamed polls (poll_revalidate_requested,poll_read_all,poll_finished,poll_closed) keep the behavior of the wrappers they replace. - Default-off
async— moq-net's default is removed and the eight sibling crates gain a default-off feature; binaries, tests, tokio glue, docs.rs metadata, and one## UnreleasedMIGRATE.mdnote per published crate all match the Impact section. No version or wire change. - IETF publisher/subscriber — the SUBSCRIBE/FETCH/TRACK_STATUS serves,
PrepareFetch/ReadFetch,Serving/RunSubscription/Retain/WritePublishDone,SubscribeTrack,RecvFill/RunFill, joining FETCH, group ingest, SUBSCRIBE_TRACKS, PUBLISH, and the namespace machines keep their old ordering,coopbudgets, guard/drop order (fillingbeforecounted, serve before PUBLISH_DONE), and withdraw/cancel paths.ReadFetch's retainedaheadandRecvFill's explicit drop order are deliberate and harmless. - Session driver —
Machine/Launch/Race/Runningpolls the halves in the old order, keeps the close → drain → abort sequence, andclosing().is_local()reads the same sharedArcthe old snapshot cloned.Client::connectowns its handshake onceConnectis built, andServer::accept'sAcceptcloses only on failure, leaving an abandoned accept to the caller (new test covers it).SendSetup,SetupAccept,Connect, andAnswerreproduce the old hold/GOAWAY/linger and deadline semantics. - Origin front —
FrontServeRun/FrontServemirrorrun_front/serve_frontstep for step, including the fresh waiter per wait, theAction::Endin-flight handoff, and theInFlightdrain. - Siblings — moq-hls (
WatchCatalog,WatchMedia,Fetch,LoadInit,Playlist,Idle,Building,Hold,Follow), moq-auth (DueTimers,Present,Answering), moq-mux (Subscribing,Opening,fmp4::poll_read,Source::subscribe), moq-statspoll_runwith its span scope, and the moq-room/moq-crypto wrappers all resolve to their previous futures. - Build gates —
coroutines.shdrops--allow, gains the manifest-path form, and covers the ten crates; the drill mutation is retargeted at the rewritten SUBSCRIBE serve.
deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit 1b399b9. No actionable findings in the reviewed scope. The poll-first direction is coherent: generated wrappers suit single waits, while explicit machines preserve progress for branching operations; retaining async HTTP edge glue fits that boundary.
Verification: targeted GitHub-only static review of the higher-risk session, publisher/subscriber, authorization, teardown, origin-front, HLS, mux, lease and stats paths, not an exhaustive review of all 165 changed files. I did not run builds, tests, benchmarks, or generated code.
Ports main's announcement listing and follow (#7678), split JS datagram tracks (#7846) and generated async wrappers (#7607) onto the Route handle: the ietf session layer takes main's poll machines with this branch's route renames, main's new tests publish through routes, and re-price checks read a fresh listing now that apps no longer see updates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Problem
Finishes the async-feature quest. moq's library crates hand-wrote an
async fn x() { kio::wait(|w| self.poll_x(w)).await }beside each poll and compiled coroutines into every build, even for callers that only poll. moq-net'sasyncfeature was on by default, and its sessions (the moq-transport layer above all) were written asasync fnbodies with no poll form, which accept policy in moq-net waits on.Approach
#[cfg_attr(feature = "async", kio::wrapper)]on its poll: 48 in moq-net, and those in moq-json, hang, moq-mux, moq-room, moq-stats, moq-crypto, and moq-auth. Each poll's doc now reads for both forms. Runtime glue and bindings that are always async (moq-tokio, moq-uring, moq-c, moq-audio, moq-transcode) use an unconditional#[kio::wrapper].asynceverywhere. moq-net'sasyncis no longer a default. hang, moq-mux, moq-hls, moq-room, moq-stats, moq-crypto, and moq-auth gain a default-offasyncfeature. Binaries, bindings, tests (self dev-dependencies, as moq-json does), and tokio glue enable them: moq-tokio'snet, moq-uring, moq-archive, and moq-mux'sasyncforwardmoq-net/async. A plaincargo check -p moq-netnow builds the poll model alone.just rs coroutinesscans kio, moq-net, moq-json, hang, moq-mux, moq-hls, moq-room, moq-stats, moq-crypto, and moq-auth, with no allowance left, socoroutines.shdrops--allow.#[kio::machine]where straight-line; progress lives in named fields, and each machine drops what the old future held at the same points.subscriber/tracks.rs), SUBSCRIBE_NAMESPACE requests (subscriber/namespaces.rs), MoQ Auth presenting and answering, and request-stream refusals. Bothhandle_streams return a poll task.ietf::Driver) is a poll machine racing its halves (SETUP, unis, bidi dispatch, datagrams, GOAWAY, auth, grant enforcement, namespace loops, the task set) in the old order with the old "only an error ends it" rule, andcrate::driver::Driverpolls it directly.Client::connect(Connect), the server handshake reply (Answer) underServer::accept,Session's and the auth handles' waits, andtime::run(nowkio::clock::drivedirectly).util::TaskSet/Tasks::pushtakekio::Tasks;util::{err_only, sendable},TaskSet::drive,auth::until, and theguardcombinators are gone, and the coding layer'sWriter::{encode, varint, close, encode_message}andStream::openremain for tests only.run_front/serve_front) is a hand-written poll machine (FrontServeRun,FrontServe), submitted to the task set as a poll task. It keeps a fresh waiter per wait, askio::waitgave it (see Follow-ups).Server::accept_requestkeeps its signature over a privateAcceptmachine;ietf::accept_setupbecomesietf::SetupAccept, lite'sSetupAcceptis reused,PausedNegotiated::okbecomes a buffered reply plusNegotiatedReply::poll, andWriter::closesplits intoclosingplus aClosingpoll so a dropped wait never resets the reply. Error codes, close-on-refusal, the 32-stream accept budget, and what a cancelled handshake releases are unchanged;an_abandoned_accept_closes_nothingcovers the cancel.Source::{catalog, resolve, subscribe_track}, the JSON entry'ssubscribe,ts::Release::new, andfmp4::Muxer::read(poll_read); hangCatalog::subscribe; moq-roomchat::Subscriber::subscribe; moq-auth's lease (Producer::dueandConsumer::reported, no more tokioNotify/select!); moq-stats'Publisher::poll_runand readers; moq-hls's export drivers (catalog and instance following, renditions, segments, init builds, media watching).Server::close,Handshake::ok,Session::{close, closed, peer_implementation}, and moq-hls'sRendition::{playlist, init, segment}.Choices I made without asking (from the agents' work, reviewed):
Consumer::{totals, presence, prefix, group}return the reader at once; with moq-dev/moq#7736's self-subscribing snapshot consumer, a refused subscription fails the first read. Gating them instead would leave a poll-only build no way to make a reader.track::Orderedgainspoll_finished(generatingfinished), so a poll-only build can wait for an ordered reader's end as for aSubscriber's.json::Consumer::from_track(madeasyncby moq-dev/moq#7736) is gated with the other async wrappers, over a privateOpeningmachine.segments::Consumer::nextkeeps a fetch in progress in the cursor, so anextdropped mid-fetch is resumed by the next call (noted in its MIGRATE).tokio::select!s in moq-hls now poll in a fixed order (closed before the backoff sleep; retirement before media readiness), and its init-build lock is akio::Shared<bool>rather than a FIFO tokioMutex.coop::Budgetpass at the top of each poll, as lite's loops do, rather than once per message; SUBSCRIBE_NAMESPACE writes are buffered per event and flushed before the next read (same bytes and order on the wire).Handshake::okstays anasync fn, so anok()future dropped before its first poll still refuses the session withCancel.poll_revalidateis renamedpoll_revalidate_requested, sorevalidate_requestedis generated.importandexportmodules whole; its defaultserverfeature enablesasync. moq-mux and moq-hls allowrustdoc::broken_intra_doc_linksonly whenasyncis off, so their docs keep linking the async-only functions on docs.rs.Impact
Breaking for Rust users who
.awaitthese crates; each published crate'sMIGRATE.mdhas an Unreleased note.features = ["async"]for sessions (Client,Server,Session),time::run, and the async wrappers. moq-tokio'snet(a default) enables it.group::Producer::readynow needs it too. New public polls:frame::Consumer::poll_read_all,track::Demand::poll_closed,track::Ordered::poll_finished.Server::acceptandtime::runarefn -> impl Futurewith unchanged inputs and outputs.asyncto keep awaiting their wrappers, including withdefault-features = false; everypoll_*stays available without it. moq-auth'sclientandserve, and moq-hls'sserver, enable it.Consumer::{totals, presence, prefix, group}drop their.await; new publicpoll_nexton the eight readers andPublisher::poll_run.fmp4::Muxer::poll_readis new;json::Consumer::from_trackneedsasync;timeline::Resume::readyborrows instead of consuming; moq-mux no longer enablesmoq-json/async. hang's and moq-json's consumer wrappers drop aT: Unpinbound.Broadcaster::{poll_closed, poll_ready},Rendition::poll_playable, andpoll_nextonrenditions::Consumerandsegments::Consumer.lease::Producer::poll_revalidateispoll_revalidate_requested.poll_read_chunkandpoll_received_resetare public, as the generated wrappers copy the poll's visibility.just test interoppasses). No versions change.Decisions
Decided 2026-11-05 (maintainer, /quest-unblock), for the earlier Waiting item on the remaining multi-await
async fnbodies:Decided 2026-11-05 (maintainer, /quest-plan), for moq-hls's import driver (
import.rs), which awaits only reqwest,tokio::fs, and retry sleeps, beside async axum handlers:async, so no poll-only build compiles it.import.rsto poll machines over boxed reqwest futures.That was the poll-first epic's last open item, so this PR also deletes
quest/m1/poll-first/README.mdand its m1 link.Follow-ups
drifted()has no live edge to rule the groups below the start stale and it asks a fill that comes back empty: a wasted round trip, not wrong data (rs/moq-net/src/model/track/miss.rs:869,:934;track.rs:1706). The front's fresh-waiter-per-wait (FrontServe::park) keeps the old scheduling, which hides it inwidened_max_age::a_fresh_reader_fills_nothing_below_its_start; droppingparkreproduces it withMOQ_SIM_SEED=7084668493435518961. Recommended fix: hold the below-start asks on a timed copy until its first frame or the arrival grace.cargo doc -p moq-auth --no-default-featuresfails on links toClient,serve::Server, andCoordinator, which exist only withclient/serve. Pre-existing; once fixed, add its poll-only doc line tojust rs coroutines.Checks
Through
quest gate:just check origin/mainon9a3f2c2709(after merging origin/main atf5764604f2): Rust 11777 tests, 11777 passed, 137 skipped.check drillfailed because thesubscriber-max-age-ignoredmutation no longer applied to the rewritten publisher;fe0da30fcfretargets it andjust test drill-sensitivitypasses.check flakeskipped: the local Nix builder cannot create user namespaces (moq-dev/moq#7763); hosted CI runs it strictly. Everything else passed.just test interoponfe0da30fcf: all checks passed.just rs featuresonfe0da30fcf:net-featuresfailed on two breaks that reproduce on origin/main (cargo clippy -p moq-net --no-default-features --lib -- -D warningson kio'sclock::Timerlarge_enum_variant, andcargo test -p moq-net --no-default-features, where moq-dev/moq#7690'sreused_encoders_start_independent_group_streamscalls the async-onlypoll_read_object).47c7a8b099fixes both, andjust rs net-features --allpasses (1391 poll-only tests). Every later step passed onfe0da30fcf, run individually:just rs coroutines(all ten crates report no coroutines),quic-features --all,crypto-key,tokio-features --all,media-features --all, the workspace--no-default-featurescheck,relay-minimal, the moq-cli, moq-bench, moq-boy, moq-auth, moq-hls/moq-rtmp/moq-rtc checks, thecapture_windowtests, and the all-features docs; the all-features clippy and tests arejust check's.-p moq-authis ambiguous in the lockfile): 265 passed.cargo check --all-targetson47c7a8b099, by manifest path for moq-auth and qmux), since feature unification hides a missingasyncin a workspace build: all pass.419b799c75(f043156f59, hand-resolved around moq-dev/moq#7678): workspace clippy with all features clean,just rs coroutinesandjust rs net-features --allpass, and workspace nextest with all features passes (11915 passed, 141 skipped).cargo fmt --all --check(dev shell) andquest check: ok.Review
Codex
gpt-6.1-sol, medium effort (quest review --base origin/main), session01a1aae6-1cc4-73e1-bbd6-a723a927f898, restarted with--newfor the design change (comment).fe0da30fcf: no findings.47c7a8b099(the two pre-existing fixes above): no findings.f043156f59(the hand-resolved merge of origin/main): no findings.1b399b927a(Claude Opus 5.5, skipped as trivial): since round 3'sf043156f59, the head only deletes the finished poll-first epic (quest/m1/poll-first/README.mdand its m1 link) for the maintainer's moq-hls decision, and mergesorigin/mainat2367707aeawithout conflicts (quest files only). No code changed;quest check --base origin/mainpasses.(Written by Claude Opus 5.5)
🤖 Generated with Claude Code