Skip to content

refactor(relay)!: the cluster polls its own work instead of spawning - #7626

Merged
kixelated merged 6 commits into
mainfrom
quest/m1/poll-first/no-spawn/cluster
Nov 2, 2026
Merged

kixelated merged 6 commits into
mainfrom
quest/m1/poll-first/no-spawn/cluster

Conversation

@kixelated

@kixelated kixelated commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Cluster polls its own work: moq-relay's Cluster spawned its origin driver and cache governor, a JoinSet of static dials, one detached tokio task per API or LAN peer, the api.url reconciler, mDNS discovery, and a detached close for each dropped LAN client. The poll-first epic wants libraries to spawn nothing and hand that work to the owner as kio::Tasks children it polls.

Approach

Follows the maintainer's recorded decisions (D2 and D5 on moq-dev/moq#7519).

  • Cluster::new returns (Cluster, Driver). Driver holds the origin driver and the cache governor as kio::Tasks children. Its poll (or run) resolves Ok once all of its work finishes, or with the first panic as "<task> panicked: <message>". Dropping it tears the origin down, so it is #[must_use].
  • Relay::run polls the driver for the whole run, startup and teardown included. A driver error goes into the cluster's Critical slot, so Started::run returns it through the main select! and the relay still closes its dials and listeners on the way out. The exit error stays origin driver panicked: ....
  • Started::run drives a Mesh: a DialMap that owns its dial tasks by kio::TaskKey, plus a kio::Tasks of peer sources (the api.url reconciler and mDNS discovery). Sources cannot reach the task set that is polling them, so they queue DialCommands on a kio::Queue, and the mesh applies them on its own poll. Replacing or releasing a dial removes its task, which cancels it.
  • Held::drop queues the LAN client's close into a polled kio::Tasks on Dialers. Started::run polls that set, and Cluster::close drains it, replacing the detached spawn and its mpsc completion channel.
  • critical::Child<T> wraps a named boxed future as a kio::Task and turns its panic into an error. The driver, the mesh, and the close set all use it.

Choices made without asking, for review:

  • Every peer dial now gets the shared --cluster-token. Before, the token a dial got depended on which source spawned it: an mDNS release that fell back to a static or API target redialed with no token. Tokens are still skipped for LAN targets (target.lan), so LAN behaviour is unchanged.
  • Any dial supervisor's panic ends the cluster. Before, only a static dial's panic did; a panic in a dynamic dial disappeared with its detached task. Each dial attempt already contains its own panic with moq_tokio::panic::isolate, so a panic that reaches the supervisor is in the redial loop itself.
  • A cancelled Started::run leaves its links' close to Cluster::close, instead of the detached close starting on its own. Relay::run always calls Cluster::close; the docs say so.

Spawns that stay: Critical still runs the stats publisher (owned by Admissions and stats are polled), ACME renewal, and the web certificate reload (both owned by the spawn audit). Session I/O is still spawned inside moq-tokio's reconnect loop, which the spawn audit owns.

The admissions quest's plan is updated for the publisher joining Driver. Cluster::new has already handed the driver out by the time with_stats runs, so the publisher needs either a queue the driver polls or a field on Options.

Peer-dial cost (D5)

Every dial supervisor now runs on the task polling Started::run instead of its own tokio task. I measured this with a one-off in-crate test, not committed. It dials n static peers at distinct paths of one in-process test_relay over loopback QUIC, on a 4-worker runtime. It records the wall time and process CPU until the cluster's /nodes view shows all n links connected, then the process CPU per second over a 2 s idle window. Medians of 10 rounds, interleaved main/branch/main/branch under quest gate. The main side is 103e98e362.

peers dial wall, main → branch dial CPU, main → branch idle CPU ms/s, main → branch
16 44.8 → 27.8 ms 91.5 → 88.7 ms 38.4 → 31.2
128 244.8 → 156.3 ms 767.5 → 569.2 ms 217.6 → 165.0
512 1212 → 632 ms 3343 → 2334 ms 503 → 429

Putting the supervisors on one task costs nothing measurable; the branch was faster and used less CPU at every size. A supervisor only awaits status changes from moq-tokio's spawned reconnect loop, and kio::Tasks re-polls only the dial that was woken. The speedup is probably from dropping one tokio spawn per peer, but I didn't break it down. The idle CPU is the post-handshake announce exchange settling in both builds, not the supervisors.

Impact

  • Public API (breaking): moq_relay::cluster::Cluster::new returns anyhow::Result<(Cluster, Driver)>, and there is a new moq_relay::cluster::Driver with poll and run. Embedders keep the driver and poll it, for example tokio::spawn(driver.run()); Relay does this itself. The migration is in rs/moq-relay/MIGRATE.md, with an entry in CHANGELOG.md. In-repo call sites are migrated: moq-cli spawns the driver into its task set, and the integration tests and benches spawn it as edges.
  • Wire: none.

Validation

  • quest gate just test -p moq-relay -p moq-cli --no-fail-fast: 668 passed, 26 skipped. This includes new tests: the driver panic stops Relay::run; the mesh dials what a source queues and fails with it; a dial panic ends the mesh; the mesh finishes once nothing is left; a critical::Child panic becomes its output. DialMap and the cache-governor tests were rewritten against the owned task set and the driver.
  • quest gate just check -p moq-relay -p moq-cli (clippy on all targets, lint, hakari): passed.
  • cargo check -p moq-relay -p moq-cli --all-targets with test-support, io-uring, stun, and iroh: passed.
  • quest check: 582 documents OK.
  • After merging origin/main (d26e2aac82) and the doc fix: quest gate just check origin/main (rust check + tests, rustdoc with -D warnings, markdown, gh, quest, shell, toml) passed.

Review

  • Reviewer: Codex (quest review), gpt-6.1-sol, medium effort, session 01a19d7b-0ad9-7983-9e46-f08ab89f636c, round 2.
  • Reviewed head 89d2aefbf63c0d609b8b14b00b691a2266d453ad against origin/main at d26e2aac82: 0 findings.
  • Round 1 (head 7c464b0fa3) also reported 0 findings.
  • Online reviews on 3f262e6d77: OpenAI found nothing actionable; pullfrog found no behavioral issues and flagged stale docs saying the cache governor is spawned at construction (Cache type docs, Config::init, Relay::load). Fixed in 89d2aefbf6: those docs now say the governor runs inside the returned Driver, only while it is polled (which Relay::run does). CodeRabbit was rate-limited and posted no review.

Follow-ups

None new. The remaining spawns already belong to Admissions and stats are polled and the spawn audit.

🤖 Generated with Claude Code

(Written by Claude Opus 5.5)

@kixelated kixelated changed the title Cluster polls its own work chore(relay): checkpoint blocked cluster polling claim Nov 2, 2026
kixelated and others added 3 commits November 2, 2026 09:32
`Cluster::new` returns `(Cluster, Driver)`: the origin driver and cache
governor run as `kio::Tasks` children of a `Driver` the owner polls, and
`Relay::run` polls it for its whole run, reporting a panic through the
cluster's critical slot so teardown still runs. `Started::run` polls every
peer dial, the `api.url` reconciler, and LAN discovery as `kio::Tasks`
children; the sources queue dial-set changes the mesh applies, and
`DialMap` owns its dial tasks by `TaskKey`. A dropped LAN dial's client
close joins a polled set instead of a detached spawn.

Each child's panic becomes an error naming it, so the relay still exits
with "origin driver panicked: ..." and similar.

Closes the quest quest/m1/poll-first/no-spawn/cluster.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title chore(relay): checkpoint blocked cluster polling claim refactor(relay)!: the cluster polls its own work instead of spawning Nov 2, 2026
@kixelated
kixelated marked this pull request as ready for review November 2, 2026 18:24
@coderabbitai

coderabbitai Bot commented Nov 2, 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 37 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
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: c56f9236-247d-404b-971c-35d8d618fdc8
Commits

Reviewing files that changed from the base of the PR and between d26e2aa and 89d2aef.

Files selected for processing: 34
  • quest/m1/poll-first/no-spawn/README.md
  • quest/m1/poll-first/no-spawn/admissions.md
  • quest/m1/poll-first/no-spawn/cluster.md
  • quest/m1/poll-first/no-spawn/sim-tests.md
  • rs/moq-cli/src/args.rs
  • rs/moq-cli/src/main.rs
  • rs/moq-relay/CHANGELOG.md
  • rs/moq-relay/MIGRATE.md
  • rs/moq-relay/benches/auth.rs
  • rs/moq-relay/benches/http_auth.rs
  • rs/moq-relay/benches/session.rs
  • rs/moq-relay/src/auth/admission_tests.rs
  • rs/moq-relay/src/cache.rs
  • rs/moq-relay/src/cluster.rs
  • rs/moq-relay/src/config.rs
  • rs/moq-relay/src/connection.rs
  • rs/moq-relay/src/critical.rs
  • rs/moq-relay/src/relay.rs
  • rs/moq-relay/src/uring.rs
  • rs/moq-relay/src/web.rs
  • rs/moq-relay/tests/integration/auth_lifetime.rs
  • rs/moq-relay/tests/integration/cluster_close.rs
  • rs/moq-relay/tests/integration/cluster_drain.rs
  • rs/moq-relay/tests/integration/cluster_idle.rs
  • rs/moq-relay/tests/integration/goaway_cluster.rs
  • rs/moq-relay/tests/integration/hidden_cluster.rs
  • rs/moq-relay/tests/integration/lan_mesh.rs
  • rs/moq-relay/tests/integration/main.rs
  • rs/moq-relay/tests/integration/memory.rs
  • rs/moq-relay/tests/integration/peer_limits.rs
  • rs/moq-relay/tests/integration/public_rules.rs
  • rs/moq-relay/tests/integration/session_revalidate.rs
  • rs/moq-relay/tests/integration/smoke.rs
  • rs/moq-relay/tests/integration/upstream_links.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

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Automated review by review (OpenAI)

Reviewed commit: 3f262e6

No actionable bugs found. The explicit cluster driver and owner-polled dial set are a reasonable fit for the poll-first direction. Reusing kio::Tasks plus a command queue avoids detached-task ownership while preserving source precedence, cancellation, panic reporting, and shutdown draining. The constructor/API migration and shared-token handling are documented; LAN targets still exclude that token.

Verification limits: static review of the changes and relevant callers/callees only. I did not run builds, tests, LAN/shutdown scenarios, or reproduce the reported peer-dial measurements.

(Written by OpenAI)

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ℹ️ No behavioral issues found. Two stale docs still describe the cache governor as spawned at construction.

Reviewed changes

  • Cluster::new returns a Driver: the origin driver and cache governor are now kio::Tasks<Child<()>> children, and poll/run report the first panic.
  • Relay::run polls the driver: it runs beside run_inner for startup and teardown, feeding driver errors into Critical::fail so Started::run still exits with origin driver panicked: ....
  • Started::run drives a Mesh: owned DialMap<Child<()>> plus kio::Tasks sources and a kio::Queue<DialCommand>, replacing the JoinSet and detached per-peer tasks.
  • Dialers closing set: Held::drop queues a Child<()> close polled by the mesh and drained by the shutdown, replacing the detached spawn and mpsc completion channel.
  • Call-site migration: moq-cli, benches, relay unit tests, integration tests, MIGRATE, CHANGELOG, and quest docs updated for the new driver contract.

I traced the task-retirement/wake behavior in kio::Tasks, the DialMap replace/release ordering against Mesh::apply and poll_closing, the lane where a cancelled Started::run defers link closes to Cluster::close, and every in-repo Cluster::new caller. No correctness issues found.

ℹ️ Docs still say the cache governor is spawned by construction

Cache's type docs and Relay::load's docs both describe the governor as starting in Cluster::new/load, but it now runs only while the returned Driver is polled. The governor docs are the user-facing contract for when it begins sampling, so they should match the new ownership.

Technical details
# Stale "spawned at construction" governor docs

## Affected sites
- `rs/moq-relay/src/cache.rs:74` — says `Cluster::new` "runs it as a critical task"; it is now a `Child` in the returned `Driver`.
- `rs/moq-relay/src/relay.rs:137` — says `Relay::load` performs "spawning the cache governor" as a startup side effect; `load` now only stores the `Driver`, and nothing runs until `Relay::run` polls it.
- `rs/moq-relay/src/cache.rs:99` (imprecise) — "The governor starts with the cluster the `Cache` is attached to"; it starts when the `Driver` is polled.

## Required outcome
- The docs say the governor (and the origin's lifecycle work) start only when the returned `Driver` is polled, and that `Relay::run` does that. No claim that `Cluster::new`/`Relay::load` spawns it.

## Suggested approach
- Rephrase both sites to name the `Driver` rather than a spawned critical task, matching `MIGRATE.md` and the updated `Cluster::new` docs.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

kixelated and others added 2 commits November 2, 2026 11:53
…olled

Cache and Relay::load still described the governor as spawned at
construction; it now lives in the Driver that Cluster::new returns and
runs only while Relay::run polls it.

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

kixelated commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator Author

Thanks pullfrog, agreed on the stale governor docs (review). Fixed in 89d2aef: the Cache type docs, Config::init, and Relay::load now say the governor moves into the Driver that Cluster::new returns and starts sampling only while that driver is polled, which Relay::run does. Relay::load no longer claims to spawn anything.

(Written by Claude Opus 5.5)

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

  • Governor docs corrected: Cache's type docs, Config::init, and Relay::load now say the headroom governor moves into the Driver returned by Cluster::new and samples only while that driver is polled (which Relay::run does). Relay::load no longer claims to spawn anything.

I confirmed the new wording against the code: Cluster::new pushes the governor as a Child into Driver.work (rs/moq-relay/src/cluster.rs:1376) and returns Driver { work } (rs/moq-relay/src/cluster.rs:1408); Relay::load_inner only stores the driver (rs/moq-relay/src/relay.rs:424) and Relay::run polls it (rs/moq-relay/src/relay.rs:621). The prior review's finding is fully addressed.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@kixelated
kixelated added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit 29f9879 Nov 2, 2026
9 of 10 checks passed
@kixelated
kixelated deleted the quest/m1/poll-first/no-spawn/cluster branch November 2, 2026 20:50
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