Skip to content

feat(net)!: model test clock, origin Driver::run(timers), and kio sheds time - #3009

Merged
kixelated merged 2 commits into
moq-uring-designfrom
moq-net-model-clock
Aug 23, 2026
Merged

feat(net)!: model test clock, origin Driver::run(timers), and kio sheds time#3009
kixelated merged 2 commits into
moq-uring-designfrom
moq-net-model-clock

Conversation

@kixelated

@kixelated kixelated commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

PR 2 of the #2875 plan (follows #3007): the model layer and the origin driver come off kio::time, and kio's time/tokio modules are deleted along with its web-async and tokio dependencies. kio is now pure waker/channel primitives, and the only reactive clock in the stack is the one the injected runtime provides.

The design came out of review discussion on top of #3007 and amends the epic plan once more:

  • The model gets no clock abstraction at all. Model time is passive measurement against the model's own stamps (group arrival vs the latency budget, cache access ticks, datagram age); nothing in the model arms a wakeup, and instants never cross between the model and runtime-armed deadlines (durations may; that invariant is documented). So the model reads a crate-internal now() that is the real clock in production and a paused clock under cfg(test): frozen at each test thread's start, moved only by an explicit test-only advance(), minting real std::time::Instants as base plus a thread-local offset. No public API, no type swap (mock_instant and friends substitute the Instant type, which is exactly what we avoid), one branch of overhead only in test builds, and isolation under both nextest and standard cargo test concurrency.
  • The origin driver is the one model component with reactive timers (route hold-down, subscription idle linger), so it takes them explicitly: origin::Driver::run(timers) installs them and returns the runnable future. It is now impossible to poll the origin without timers, by construction.
  • Runtime splits into Timers + Runtime. Timer minting and the clock move to a Timers supertrait, so the origin can borrow a runtime's timers without pretending to have a transport. Downstream Tokio tests dev-depend on the shared moq-tokio adapter instead of carrying local timer copies.

API

moq_net::runtime (re-exported: moq_net::{Runtime, Timers})

/// std::time::Instant on native; a performance.now()-backed shim on wasm.
pub type Instant = std::time::Instant;

/// One re-armable timer registration. Arming is synchronous and in-memory.
pub trait Timer {
    /// Arm, re-arm, or disarm (None). Re-arming an elapsed timer for a later
    /// instant makes it pend again; for a past instant it stays elapsed.
    fn set(&mut self, at: Option<Instant>);
    /// Ready once the armed instant has passed; registers the waiter otherwise.
    /// Fused: elapsed keeps reporting Ready until re-armed; disarmed never fires.
    fn poll(&mut self, waiter: &kio::Waiter) -> Poll<()>;
}

/// The timer half of a runtime: mint Timers and read the clock they follow.
/// Clones must be cheap (a ZST or a refcount).
pub trait Timers: Clone {
    type Timer: Timer;
    fn timer(&self) -> Self::Timer;
    /// Defaults to the real clock. Only virtual-time test runtimes override it;
    /// it exists because relative arming (now + interval) must read the same
    /// clock the timers fire on.
    fn now(&self) -> Instant { Instant::now() }
}

/// The executor a session runs on: Timers plus a transport and a spawn.
/// No Send bounds anywhere: pinning one transport type per runtime lets each
/// impl know the concrete Machine it spawns, including whether it is Send.
pub trait Runtime: Timers {
    type Transport: transport::poll::Session;
    /// Own the session's protocol machine and poll it to completion.
    fn spawn(&self, machine: Machine<Self>);
}

/// The ergonomic deadline over Timer: idempotent set, fused poll.
pub struct Deadline<R: Timers>;
impl<R: Timers> Deadline<R> {
    pub fn new(runtime: &R) -> Self;                       // disarmed
    pub fn at(runtime: &R, at: Instant) -> Self;
    pub fn after(runtime: &R, duration: Duration) -> Self; // vs runtime.now()
    pub fn set(&mut self, at: Option<Instant>);            // same instant: no-op
    pub fn deadline(&self) -> Option<Instant>;
    pub fn poll(&mut self, waiter: &kio::Waiter) -> Poll<()>;
    pub async fn wait(&mut self);
}

/// The future driving a Session's protocol state; created by connect/accept and
/// handed straight to Runtime::spawn. Resolves when the session ends.
pub struct Machine<R: Runtime>;   // impl Future<Output = Result<(), Error>>
impl<R: Runtime> Machine<R> {
    pub fn poll(&mut self, waiter: &kio::Waiter) -> Poll<Result<(), Error>>;
}

Behind the test-runtime feature:

/// Deterministic runtime: nothing runs and no time passes unless the test says so.
pub struct Test<S: transport::poll::Session = Never>;  // Clone shares clock + queue
impl Test<S> {
    pub fn new() -> Self;                  // virtual clock starts at the real now
    pub fn advance(&self, d: Duration);    // fires elapsed timers; no auto-advance
    pub fn advance_to_timer(&self) -> bool; // jump to the earliest armed timer
    pub fn tick(&self) -> usize;           // poll every spawned machine once
}
// impl Timers + Runtime for Test<S>

/// An uninhabited transport, for Test runtimes that never open a session.
pub enum Never {}

moq_net::origin (breaking)

// Producer::new(info) -> (Producer, Driver) is unchanged.
impl Driver {
    /// Install the timers and return the runnable driver. The origin makes no
    /// progress (and its linger/hold-down deadlines cannot exist) until this.
    pub fn run<T>(self, timers: T) -> Run
    where T: Timers + Send + Sync + 'static, T::Timer: Send + 'static; // bounds relax on wasm
}
pub struct Run;  // impl Future<Output = ()> + fn poll(&mut self, &kio::Waiter);
                 // dropping it tears the origin down, exactly as Driver did
// Driver no longer implements Future: polling without timers is unrepresentable.

moq_tokio::runtime

/// Tokio runtime handle: machines are tokio::spawn'ed, timers are tokio sleeps,
/// now() reads tokio's pausable clock. ZST. The default S = () makes a bare
/// Runtime::new() a transportless Timers handle (what origin::spawn uses).
pub struct Runtime<S = ()>;       // impl Timers for any S; impl Runtime for S: Session

/// Hands the machine back instead of spawning, for callers that drive the
/// session inline (the relay's WebSocket handler).
pub struct Inline<S: Session>;    // fn new(); fn take(&self) -> Option<Machine<Self>>

pub struct Timer;                 // the tokio-sleep Timer both hand out

moq_tokio::origin::spawn and the rest of moq-tokio's public surface are unchanged; moq_wasm::runtime::Runtime (browser: microtask spawn, wasmtimer timers) likewise implements the split traits.

kio (breaking)

kio::time and kio::tokio are deleted, along with the time/tokio features and the web-async and tokio dependencies. kio is waker/channel primitives only.

(Everything else is crate-private: the model's paused test clock and the type-erased late-bound timers behind Driver::run add no public surface.)

Wire behavior changes

None. No draft or JS changes.

Test plan

  • nix develop --command just fix / CI-strict just check / CI-strict just test (3,473 Rust tests passed, 2 skipped; 52 Python tests passed)
  • Model timing tests (track datagram age, max-age expiry, resume, cache) migrate from tokio::time::pause/advance to the model test clock's advance; origin driver tests keep their tokio paused-clock semantics through .run(tokio_test::Tokio::<()>::new()); runtime/test.rs and model/clock.rs carry their own unit tests. Regression coverage verifies test-thread clock isolation and rebases pre-run handover holds onto the installed driver clock. No assertion was weakened or dropped.

One inference note for reviewers: a defaulted type parameter does not drive expression inference, so the transportless handles are written Runtime::<()>::new() rather than a bare Runtime::new().

(Written by Claude Fable 5)

🤖 Generated with Claude Code

https://claude.ai/code/session_01BCxzHiGxN5qi8Gd687nsgm

(Updated by GPT-5)

…t timers

The model layer never arms a wakeup: its time is passive measurement against
its own stamps (group arrival vs the latency budget, cache access ticks,
datagram age). So it gets no clock abstraction and no runtime handle. It reads
a crate-internal now() that is the real clock in production and, under
cfg(test), a paused clock moved only by an explicit advance(), minting real
std::time::Instants as base plus offset. No public API, no Instant type swap,
and safe as process-global state because nextest runs one process per test.

The origin driver is the one model component with reactive timers (route
hold-down, subscription idle linger), so it takes them explicitly: Runtime
splits into a Timers supertrait (timer + clock) plus Runtime (transport +
spawn), and origin::Driver::run(timers) installs a type-erased copy in the
origin's shared state and returns the runnable future. Driver no longer
implements Future, so polling the origin without timers is unrepresentable;
the serve tasks fetch their timers lazily at first poll, which is provably
after run installs them. moq_tokio::runtime::Runtime<S = ()> doubles as the
transportless Timers handle, and moq_tokio::origin::spawn keeps its signature.

With that, kio::time and kio::tokio are deleted along with kio's web-async and
tokio dependencies: kio is waker and channel primitives only, and the only
reactive clock in the stack is the one the injected runtime provides. The
invariant that makes the two remaining clock domains safe is documented:
instants never cross between model stamps and runtime-armed deadlines,
durations may.

Part of #2875; follows #3007.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BCxzHiGxN5qi8Gd687nsgm

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fa1988d73d

ℹ️ 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".

Comment thread rs/moq-net/src/model/clock.rs Outdated
Comment thread rs/moq-net/src/runtime.rs Outdated
Comment thread rs/moq-hls/src/lib.rs Outdated
Make the passive model test clock thread-local so cargo test concurrency cannot age another test. Start pre-run handover holds only after the driver clock is installed, and reuse moq-tokio's timer adapter across downstream tests.

Co-Authored-By: GPT-5 <noreply@openai.com>

Copy link
Copy Markdown
Collaborator Author

@codex review

Addressed the three prior findings in 514daba and added regressions for the clock issues. (Written by GPT-5)

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: 514dabaf26

ℹ️ 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".

@kixelated
kixelated merged commit d97da97 into moq-uring-design Aug 23, 2026
5 checks passed
@kixelated
kixelated deleted the moq-net-model-clock branch August 23, 2026 03:10
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