Skip to main content

moqtap_proxy/
release_timer.rs

1//! A process-wide release wheel: one dedicated OS thread, one min-heap of
2//! deadlines, one `CancellationToken` per armed deadline.
3//!
4//! `tokio::time::sleep` cannot be used for release timing. Tokio parks its
5//! runtime on a wait with a millisecond timeout, and on Windows every
6//! *interruptible* wait is quantised to the ~15.6 ms system tick.
7//! `std::thread::sleep` is
8//! the exception — std implements it with a high-resolution waitable timer,
9//! so it lands within ~0.3 ms — but it cannot be woken early.
10//!
11//! This module recovers *accurate *and* interruptible* in plain safe code: the
12//! thread sleeps in [`SLICE`]-bounded steps and re-reads the heap between them,
13//! and parks on an untimed `Condvar` — which *is* woken promptly — when there
14//! is nothing to wait for.
15//!
16//! Nothing here is `pub`, the whole module is safe code, and there is no
17//! `#[cfg]` outside its test module: one implementation runs on every
18//! platform. That is what lets this file move into `quinn-netem`, give
19//! each impaired socket its own wheel, or be replaced wholesale, without
20//! any of it being a breaking change.
21
22use std::cmp::{Ordering, Reverse};
23use std::collections::BinaryHeap;
24use std::sync::{Arc, Condvar, Mutex, OnceLock, Weak};
25use std::thread::JoinHandle;
26use std::time::{Duration, Instant};
27use tokio_util::sync::CancellationToken;
28
29use crate::instrument::TimerBackend;
30
31/// How long the release thread may sleep without re-reading the heap.
32///
33/// This is a responsiveness knob, not an accuracy one: it sets how fast a
34/// newly armed *nearer* deadline is noticed, never the accuracy of one the
35/// wheel already holds. The last sleep before a deadline is
36/// `min(remaining, SLICE) == remaining`, so accuracy comes from
37/// `std::thread::sleep` whatever this constant is.
38const SLICE: Duration = Duration::from_micros(500);
39/// Heap length below which the sweep never runs.
40///
41/// Below this many entries the residue is bounded and cheap (~40 bytes a
42/// slot), and rebuilding a short heap on a timer would trade that bounded
43/// residue for an unbounded `retain` rate.
44const SWEEP_MIN_LEN: usize = 64;
45/// Minimum interval between two sweeps.
46const SWEEP_INTERVAL: Duration = Duration::from_millis(250);
47
48/// A registered deadline. Cheap to clone (one `Arc` + one token clone).
49///
50/// Dropping every clone abandons the deadline: the wheel drops the slot at
51/// its own instant, or at the next sweep, whichever comes first. There is
52/// no unregister call.
53#[derive(Clone, Debug)]
54pub(crate) struct Deadline {
55    entry: Arc<Entry>,
56}
57
58impl Deadline {
59    /// The token the wheel cancels once `Instant::now() >= at`.
60    ///
61    /// Level-triggered: awaiting it after the wheel has fired resolves
62    /// immediately, so the engine may create and drop `token().cancelled()`
63    /// on every `select!` iteration without a lost-wakeup race and without
64    /// re-registering with the wheel.
65    pub(crate) fn token(&self) -> &CancellationToken {
66        &self.entry.token
67    }
68
69    /// A deadline that has already passed. Allocates one token, never
70    /// touches the wheel, never starts the release thread.
71    fn already_due() -> Self {
72        let token = CancellationToken::new();
73        token.cancel();
74        Self { entry: Arc::new(Entry { token }) }
75    }
76}
77
78/// Register `at` with the process-wide wheel.
79///
80/// **The sole lazy-initialisation trigger for the release thread.** When
81/// `at <= Instant::now()` it returns an already-cancelled [`Deadline`]
82/// without constructing the wheel, so `Delay { by: 0 }`, a delay swallowed
83/// by the monotonic clamp, and every session that never delays all leave
84/// [`crate::instrument::release_timer_started`] `false`.
85pub(crate) fn arm_at(at: Instant) -> Deadline {
86    if at <= Instant::now() {
87        return Deadline::already_due();
88    }
89    shared().arm(at)
90}
91
92/// The process-wide wheel, constructed on first use. Never dropped: it lives in
93/// a `static OnceLock<ReleaseTimer>`, so its thread is never joined and the
94/// *atexit joins a thread that holds a lock* hazard cannot arise.
95fn shared() -> &'static ReleaseTimer {
96    static SHARED: OnceLock<ReleaseTimer> = OnceLock::new();
97    SHARED.get_or_init(|| {
98        let timer = ReleaseTimer::new();
99        // Published here and not in `ReleaseTimer::new`, which also runs
100        // for owned wheels (this module's tests, any per-socket owner)
101        // that are not the process-wide one.
102        crate::instrument::note_release_timer_started(timer.backend());
103        timer
104    })
105}
106
107/// Whether the process-wide wheel has been constructed.
108///
109/// Read by this module's tests; every non-test caller in the crate wants
110/// [`backend`], which answers the same question and says which one.
111#[allow(dead_code)]
112pub(crate) fn started() -> bool {
113    crate::instrument::release_timer_started()
114}
115
116/// Which primitive the process-wide wheel's thread waits on, or `None` if
117/// it was never constructed. This is what
118/// [`crate::instrument::release_timer_backend`] returns.
119pub(crate) fn backend() -> Option<TimerBackend> {
120    crate::instrument::release_timer_backend()
121}
122
123/// An owned release wheel: one OS thread, one heap, one parker.
124///
125/// The process-wide wheel is one of these in a `OnceLock`. Instances
126/// created directly (this module's tests today, per-socket owners later)
127/// join their thread on drop — unless the drop is itself running on that
128/// thread, which the `Drop` impl below explains.
129pub(crate) struct ReleaseTimer {
130    inner: Arc<Inner>,
131    thread: Option<JoinHandle<()>>,
132}
133
134impl ReleaseTimer {
135    /// Resolve a backend and start the release thread.
136    ///
137    /// Honours `MOQTAP_RELEASE_TIMER` (`slice` | `condvar`), read once,
138    /// here. Unset or unrecognised means `slice`. **Cannot fail**, and
139    /// that is a property of the design rather than an omission: there is
140    /// no OS object to fail to create, so there is no fallback ladder and
141    /// no error path to test.
142    pub(crate) fn new() -> Self {
143        let parker = match std::env::var("MOQTAP_RELEASE_TIMER").as_deref() {
144            Ok("condvar") => Parker::Condvar,
145            // An unrecognised value resolves to the sliced sleeper rather
146            // than panicking: this is a diagnostic knob read inside a
147            // forwarding task, and `backend()` reports what was actually
148            // chosen, so a typo is observable without being fatal.
149            _ => Parker::SleepSlice,
150        };
151        let inner = Arc::new(Inner {
152            state: Mutex::new(State {
153                heap: BinaryHeap::new(),
154                seq: 0,
155                stopping: false,
156                wakes: 0,
157                last_sweep: Instant::now(),
158            }),
159            cv: Condvar::new(),
160            parker,
161        });
162        let thread_inner = Arc::clone(&inner);
163        let thread = std::thread::Builder::new()
164            .name("moqtap-release".to_owned())
165            .spawn(move || run(thread_inner))
166            .expect("release timer thread");
167        Self { inner, thread: Some(thread) }
168    }
169
170    /// Register `at`. `at <= now` yields an already-cancelled [`Deadline`]
171    /// without touching the heap or waking the thread.
172    pub(crate) fn arm(&self, at: Instant) -> Deadline {
173        if at <= Instant::now() {
174            return Deadline::already_due();
175        }
176        let entry = Arc::new(Entry { token: CancellationToken::new() });
177        {
178            let mut g = self.inner.state.lock().expect("release wheel state");
179            g.seq += 1;
180            let seq = g.seq;
181            g.heap.push(Reverse(Slot { at, seq, entry: Arc::downgrade(&entry) }));
182        } // guard released before notify
183        self.inner.cv.notify_one(); // a no-op when nobody waits
184        Deadline { entry }
185    }
186
187    /// Which primitive this wheel's thread waits on.
188    pub(crate) fn backend(&self) -> TimerBackend {
189        match self.inner.parker {
190            Parker::SleepSlice => TimerBackend::SleepSlice,
191            Parker::Condvar => TimerBackend::Condvar,
192        }
193    }
194
195    /// Heap length, including slots whose [`Deadline`] was dropped and
196    /// that have not been popped or swept yet.
197    #[cfg(test)]
198    pub(crate) fn debug_len(&self) -> usize {
199        self.inner.state.lock().expect("release wheel state").heap.len()
200    }
201
202    /// Loop iterations the release thread has completed — one per wake.
203    /// Lets a test assert that an empty wheel costs nothing: the count must
204    /// not move across an idle window.
205    #[cfg(test)]
206    pub(crate) fn debug_wakes(&self) -> u64 {
207        self.inner.state.lock().expect("release wheel state").wakes
208    }
209}
210
211/// Sets `stopping`, wakes the thread, and joins it — except when the drop
212/// is itself running *on* that thread, where it detaches instead. Pending
213/// deadlines are abandoned, not fired: a dropped wheel has no listeners
214/// left.
215///
216/// # Why the join is conditional
217///
218/// From the release thread the join is not a slow path, it is a permanent
219/// deadlock. Step 3 of [`run`] cancels tokens *outside* the lock and *on*
220/// the release thread, so every waker registered on a deadline — which is
221/// what awaiting `token().cancelled()` installs — runs there too. A waker
222/// that drops the last handle to the wheel enters this function on the
223/// release thread, and `join()` then waits for the thread executing it.
224/// `stopping` and `notify_all` do not rescue it: the thread is inside the
225/// cancel loop and can never return to the top of `run` to read the flag.
226///
227/// Detaching there is not a compromise, it is the only outcome that
228/// terminates. `stopping` is set and `notify_all` sent before the branch,
229/// so the detached thread finishes the cancel loop it is in, sees the flag
230/// on its next iteration and returns on its own: a bounded, self-clearing
231/// thread rather than an unbounded hang. The handle already carries the
232/// id `spawn` gave the thread, so no separate field is needed to
233/// recognise it.
234impl Drop for ReleaseTimer {
235    fn drop(&mut self) {
236        {
237            let mut g = self.inner.state.lock().expect("release wheel state");
238            g.stopping = true;
239        }
240        self.inner.cv.notify_all();
241        if let Some(handle) = self.thread.take() {
242            if handle.thread().id() == std::thread::current().id() {
243                return; // detach: joining here would join this thread to itself
244            }
245            let _ = handle.join();
246        }
247    }
248}
249
250// ── internals ──────────────────────────────────────────────────────────
251
252/// What a [`Deadline`] and its heap slot share.
253#[derive(Debug)]
254struct Entry {
255    token: CancellationToken,
256}
257
258/// A heap slot. `Weak`, so a dropped [`Deadline`] costs a skipped pop
259/// rather than a token nobody holds.
260struct Slot {
261    at: Instant,
262    seq: u64,
263    entry: Weak<Entry>,
264}
265
266// `Weak` is neither `Ord` nor `Eq`, so these cannot be derived. Order is
267// `(at, seq)`, `seq` monotonic, so equal instants pop in registration
268// order. The heap is a `BinaryHeap<Reverse<Slot>>`, i.e. a *min*-heap:
269// inverting either half of this pair turns the wheel into a LIFO that
270// serves the farthest deadline first, and nothing but a wrong-looking
271// timing test would say so — which is why
272// `the_heap_is_a_min_heap_and_ties_break_by_registration` below tests it
273// purely.
274impl PartialEq for Slot {
275    fn eq(&self, o: &Self) -> bool {
276        (self.at, self.seq) == (o.at, o.seq)
277    }
278}
279impl Eq for Slot {}
280impl Ord for Slot {
281    fn cmp(&self, o: &Self) -> Ordering {
282        (self.at, self.seq).cmp(&(o.at, o.seq))
283    }
284}
285impl PartialOrd for Slot {
286    fn partial_cmp(&self, o: &Self) -> Option<Ordering> {
287        Some(self.cmp(o))
288    }
289}
290
291/// Everything the release thread and its registrars share under one lock.
292struct State {
293    /// A *min*-heap of armed deadlines.
294    heap: BinaryHeap<Reverse<Slot>>,
295    /// Monotonic registration counter, the tie-break in [`Slot`]'s `Ord`.
296    seq: u64,
297    /// Set by [`ReleaseTimer`]'s `Drop`; the thread returns on its next
298    /// wake.
299    stopping: bool,
300    /// Loop iterations. Always compiled — it is one `u64` increment under
301    /// a lock the thread already holds — and read only by `debug_wakes`.
302    /// The `+=` counts as a read, so `dead_code` does not fire on it in a
303    /// non-test build.
304    wakes: u64,
305    /// When the heap was last rebuilt to drop abandoned slots.
306    last_sweep: Instant,
307}
308
309/// The shared half of a wheel: what the thread and its registrars both
310/// hold an `Arc` to.
311struct Inner {
312    state: Mutex<State>,
313    cv: Condvar,
314    parker: Parker,
315}
316
317/// How the thread waits when a deadline is armed.
318///
319/// **Exactly one implementation ships.** `SleepSlice` is the backend on
320/// every platform. `Condvar` exists because [`TimerBackend::Condvar`] and
321/// `ImpairmentKind::CoarseReleaseTimer` are public API and must be
322/// reachable by a test: forcing it on Windows produces a genuinely
323/// tick-bound wheel, which is exactly what that impairment reports, so the fault
324/// injection is not a simulation. **No `#[cfg]`: both arms compile on
325/// every platform**, and the choice is made once, at construction, from
326/// `MOQTAP_RELEASE_TIMER`. This enum is the seam for adding a platform
327/// timer without touching the wheel.
328enum Parker {
329    /// `std::thread::sleep(min(remaining, SLICE))`, re-reading the heap
330    /// between slices.
331    SleepSlice,
332    /// `Condvar::wait_timeout` for the whole remainder: interruptible
333    /// everywhere, high-resolution only on Unix.
334    Condvar,
335}
336
337/// What the thread should do next. **Pure** — no clock read, no lock — so
338/// `the_wait_plan_never_overshoots` pins the never-overshoot rule without
339/// timing anything.
340#[derive(Debug, PartialEq, Eq)]
341enum Wait {
342    /// The head is due now; fire it before waiting again.
343    Fire,
344    /// Nothing is armed; park on the condvar with no timeout.
345    Idle,
346    /// Sleep this long, then re-read the heap.
347    Sleep(Duration),
348}
349
350/// Decide the next wait from a clock reading and the head of the heap.
351///
352/// Never returns a `Sleep` that runs past `next`: that, plus the
353/// `slot.at <= now` re-check in [`run`], is the whole never-fire-early
354/// guarantee.
355fn plan(now: Instant, next: Option<Instant>, slice: Duration) -> Wait {
356    match next {
357        None => Wait::Idle,
358        Some(at) => match at.checked_duration_since(now) {
359            None => Wait::Fire,                   // overdue
360            Some(d) if d.is_zero() => Wait::Fire, // due exactly now
361            Some(d) => Wait::Sleep(d.min(slice)), // never sleeps past `at`
362        },
363    }
364}
365
366/// The release thread's body.
367fn run(inner: Arc<Inner>) {
368    let mut g = inner.state.lock().expect("release wheel state");
369    loop {
370        g.wakes += 1;
371        if g.stopping {
372            return;
373        }
374        let now = Instant::now();
375
376        // 1. Collect everything due. `s.at <= now` is the whole
377        //    never-fire-early guarantee; nothing else enforces it.
378        let mut fired: Vec<Arc<Entry>> = Vec::new();
379        while g.heap.peek().is_some_and(|Reverse(s)| s.at <= now) {
380            let Reverse(slot) = g.heap.pop().expect("peeked a due slot");
381            if let Some(e) = slot.entry.upgrade() {
382                fired.push(e);
383            }
384        }
385
386        // 2. Sweep abandoned slots. `strong_count()` does not create an
387        //    `Arc`, so nothing can be dropped under the lock here.
388        if g.heap.len() > SWEEP_MIN_LEN
389            && now.saturating_duration_since(g.last_sweep) >= SWEEP_INTERVAL
390        {
391            g.heap.retain(|Reverse(s)| s.entry.strong_count() > 0);
392            g.last_sweep = now;
393        }
394
395        let next = g.heap.peek().map(|Reverse(s)| s.at);
396
397        // 3. Cancel *outside* the lock, then restart the iteration with a
398        //    fresh clock. `cancel()` runs registered wakers; running them
399        //    under the process-global wheel mutex would put scheduler code
400        //    inside it, and a waker that re-armed would deadlock. The
401        //    `Arc<Entry>`s are dropped out here too, for the same reason.
402        if !fired.is_empty() {
403            drop(g);
404            for e in fired {
405                e.token.cancel();
406            }
407            g = inner.state.lock().expect("release wheel state");
408            continue; // `next` is now stale
409        }
410
411        // 4. Wait. The guard was NOT released above, so `next` is current
412        //    and no arm can have slipped in between.
413        match plan(Instant::now(), next, SLICE) {
414            Wait::Fire => continue,
415            Wait::Idle => {
416                g = inner.cv.wait(g).expect("release wheel state");
417            }
418            Wait::Sleep(d) => match inner.parker {
419                // A `notify_one` that lands here is lost on purpose: the
420                // thread is not waiting, and it re-reads the heap within
421                // `d <= SLICE` anyway.
422                Parker::SleepSlice => {
423                    drop(g);
424                    std::thread::sleep(d);
425                    g = inner.state.lock().expect("release wheel state");
426                }
427                // Forced-coarse: interruptible but tick-bound on Windows.
428                // It waits the *whole* remainder rather than a slice —
429                // slicing a `wait_timeout` would cost ~15 ms per slice.
430                Parker::Condvar => {
431                    let rest = next
432                        .expect("Wait::Sleep implies an armed head")
433                        .saturating_duration_since(Instant::now());
434                    g = inner.cv.wait_timeout(g, rest).expect("release wheel state").0;
435                }
436            },
437        }
438    }
439}
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444    use std::future::Future;
445    // Aliased: `Ordering` in this module is `std::cmp::Ordering`, which
446    // `Slot` needs.
447    use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
448    use std::task::{Context, Poll, Wake, Waker};
449
450    // Every test here owns its wheel: it constructs `ReleaseTimer::new()`,
451    // arms against that instance and drops it. None calls `arm_at`,
452    // `shared()` or `started()` in a way that constructs the process-wide
453    // wheel. `cargo test` runs these concurrently with the other modules'
454    // unit tests in one process, so a shared heap would make `debug_len()`
455    // a race — and an owned wheel is torn down through `Drop`, so every run
456    // also exercises the instance API per-socket owners inherit, on both of
457    // that impl's branches.
458
459    /// Nearest-rank percentile of an already-sorted slice.
460    fn pct(sorted: &[Duration], q: usize) -> Duration {
461        assert!(!sorted.is_empty(), "no samples");
462        let rank = (sorted.len() * q).div_ceil(100);
463        sorted[rank.saturating_sub(1).min(sorted.len() - 1)]
464    }
465
466    /// Sort in place and return `(p50, p95, p99, max)`.
467    fn quantiles(samples: &mut [Duration]) -> (Duration, Duration, Duration, Duration) {
468        samples.sort_unstable();
469        (pct(samples, 50), pct(samples, 95), pct(samples, 99), pct(samples, 100))
470    }
471
472    /// The invariant every other release guarantee rests on: a
473    /// wheel that fires early is a wheel that reorders bytes. Structural,
474    /// no timing budget, cannot flake.
475    ///
476    /// Arming one at a time also makes this the coverage of the
477    /// empty-wheel `Condvar::wait` -> `notify_one` path, since the wheel
478    /// is idle before every sample.
479    #[tokio::test]
480    async fn a_deadline_never_fires_early() {
481        let timer = ReleaseTimer::new();
482        for i in 0..300u64 {
483            // Deterministic spacings over 0.2-5 ms; 1601 is coprime with
484            // 4801, so 300 samples are 300 distinct durations.
485            let micros = 200 + (i * 1601) % 4801;
486            let at = Instant::now() + Duration::from_micros(micros);
487            let d = timer.arm(at);
488            d.token().cancelled().await;
489            let observed = Instant::now();
490            assert!(
491                observed >= at,
492                "sample {i} ({micros} us) fired {:?} early",
493                at.saturating_duration_since(observed)
494            );
495        }
496    }
497
498    /// Block until the release thread has completed `n` more iterations.
499    ///
500    /// Bounded, and it returns rather than panicking when it gives up: a
501    /// wheel that stops waking is what the callers' own assertions are
502    /// about, and each of them says more about it than this could.
503    fn wait_for_wakes(timer: &ReleaseTimer, n: u64) {
504        let want = timer.debug_wakes() + n;
505        let give_up = Instant::now() + Duration::from_secs(1);
506        while timer.debug_wakes() < want && Instant::now() < give_up {
507            std::thread::sleep(Duration::from_micros(200));
508        }
509    }
510
511    /// The property `SLICE` exists to provide: a nearer deadline
512    /// armed while the thread is already waiting on a far one is noticed
513    /// within one slice.
514    #[tokio::test]
515    async fn an_earlier_deadline_registered_later_preempts_the_current_wait() {
516        let timer = ReleaseTimer::new();
517        let far = timer.arm(Instant::now() + Duration::from_secs(2));
518        // The near deadline only arrives mid-wait if the thread is already
519        // in its slice loop, and two completed iterations say it is: the
520        // first is the wake that saw `far` armed, and a second can only
521        // follow a slice that ran out.
522        wait_for_wakes(&timer, 2);
523
524        let start = Instant::now();
525        let near_at = start + Duration::from_millis(5);
526        let near =
527            std::thread::scope(|s| s.spawn(|| timer.arm(near_at)).join().expect("arming thread"));
528        near.token().cancelled().await;
529        let elapsed = start.elapsed();
530
531        assert!(!far.token().is_cancelled(), "the +2 s deadline fired before the +5 ms one");
532        assert!(
533            elapsed < Duration::from_millis(500),
534            "the nearer deadline took {elapsed:?} to be noticed"
535        );
536    }
537
538    /// An empty wheel costs nothing: the thread parks on an
539    /// untimed `Condvar::wait`, not on a polling timeout.
540    ///
541    /// The one wake it allows is the post-fire iteration — the `continue`
542    /// in step 3 of `run`, which cancels the fired tokens and then goes
543    /// round once more to find the heap empty and park. So the last token
544    /// can resolve one iteration before the thread is asleep, and it is
545    /// exactly one iteration, because that one finds nothing armed and
546    /// takes the untimed `wait`.
547    #[tokio::test]
548    async fn the_release_thread_parks_when_the_wheel_is_empty() {
549        let timer = ReleaseTimer::new();
550        let at = Instant::now() + Duration::from_millis(5);
551        let armed: Vec<Deadline> = (0..100).map(|_| timer.arm(at)).collect();
552        for d in &armed {
553            d.token().cancelled().await;
554        }
555
556        /// The window the negative claim is made over. A window is the
557        /// only evidence there is for "nothing happened", so this one is
558        /// a duration and stays one.
559        const IDLE: Duration = Duration::from_millis(200);
560
561        let before = timer.debug_wakes();
562        tokio::time::sleep(IDLE).await;
563        let after = timer.debug_wakes();
564
565        assert!(
566            after - before <= 1,
567            "the thread woke {} times over an idle {IDLE:?} window; at most one is the \
568             post-fire iteration parking",
569            after - before
570        );
571        assert_eq!(timer.debug_len(), 0, "fired slots stayed in the heap");
572    }
573
574    /// Abandoned slots are swept rather than carried to their own
575    /// instants.
576    ///
577    /// The deadlines are far-dated on purpose: 30 s is the case the sweep
578    /// exists for. It is `max_hold`'s default, and a torn-down `Hold` is
579    /// exactly how a dead slot survives its own instant.
580    ///
581    /// This assumes the shipping [`Parker::SleepSlice`]. Under a forced
582    /// `MOQTAP_RELEASE_TIMER=condvar` the thread waits out the *whole*
583    /// remainder, so with only a +30 s head it does not wake to sweep at
584    /// all and this test fails by design — which is one more reason no
585    /// test in this binary may set that variable.
586    #[tokio::test]
587    async fn abandoned_far_deadlines_are_swept() {
588        let timer = ReleaseTimer::new();
589        let far = Instant::now() + Duration::from_secs(30);
590        for _ in 0..10_000 {
591            drop(timer.arm(far));
592        }
593        assert!(timer.debug_len() >= SWEEP_MIN_LEN, "the abandoned slots never reached the heap");
594
595        // Give the thread a reason to wake.
596        let wake = timer.arm(Instant::now() + Duration::from_millis(5));
597        wake.token().cancelled().await;
598
599        let give_up = Instant::now() + Duration::from_secs(1);
600        loop {
601            let len = timer.debug_len();
602            if len < SWEEP_MIN_LEN {
603                break;
604            }
605            assert!(Instant::now() < give_up, "{len} abandoned slots still in the heap after 1 s");
606            tokio::time::sleep(Duration::from_millis(10)).await;
607        }
608    }
609
610    /// The instance API per-socket owners inherit actually stops, rather
611    /// than waiting out its deadlines.
612    #[test]
613    fn an_instance_timer_joins_its_thread_on_drop() {
614        let timer = ReleaseTimer::new();
615        let held = timer.arm(Instant::now() + Duration::from_secs(10));
616        let t0 = Instant::now();
617        drop(timer);
618        let elapsed = t0.elapsed();
619        drop(held);
620        assert!(
621            elapsed < Duration::from_millis(250),
622            "dropping a wheel with a +10 s deadline took {elapsed:?}"
623        );
624    }
625
626    /// A waker that tears the wheel down when it fires: it owns the only
627    /// [`ReleaseTimer`] handle and drops it from inside `wake`.
628    /// Records where it ran and whether the drop returned, so the test can tell
629    /// "the drop is stuck" from *the waker never fired*.
630    struct DropTheWheelOnWake {
631        wheel: Mutex<Option<ReleaseTimer>>,
632        fired_on: Mutex<Option<String>>,
633        returned: AtomicBool,
634    }
635
636    impl Wake for DropTheWheelOnWake {
637        fn wake(self: Arc<Self>) {
638            self.wake_by_ref();
639        }
640        fn wake_by_ref(self: &Arc<Self>) {
641            *self.fired_on.lock().expect("waker state") =
642                Some(std::thread::current().name().unwrap_or("<unnamed>").to_owned());
643            // The lock guard is a temporary and is released at the end of
644            // this statement, so the wheel is dropped without it held.
645            let wheel = self.wheel.lock().expect("waker state").take();
646            drop(wheel); // the last handle: this runs `ReleaseTimer::drop`
647            self.returned.store(true, AtomicOrdering::SeqCst);
648        }
649    }
650
651    /// Tearing the wheel down from inside a deadline's own completion path
652    /// returns instead of deadlocking.
653    ///
654    /// Step 3 of [`run`] cancels tokens on the release thread, so a waker
655    /// registered on a deadline runs *there*, and a waker that drops the
656    /// last handle to the wheel therefore enters `Drop` on the release
657    /// thread. An unconditional `handle.join()` would then wait for the
658    /// very thread executing it, forever: `stopping` is already set and
659    /// `notify_all` already sent, and neither helps, because the thread is
660    /// inside the cancel loop and cannot reach the top of `run` to read
661    /// the flag.
662    ///
663    /// The waker is an ordinary [`Wake`] implementation registered by
664    /// polling the same `cancelled()` future any awaiting task would, so
665    /// nothing here is a shape only a test produces. Two assertions keep
666    /// it honest: the first poll must return `Pending`, or no waker was
667    /// registered and the drop never ran from a wake at all; and the wake
668    /// must have landed on the release thread, or the self-join was not
669    /// exercised even though the timing passed.
670    ///
671    /// The bound is a liveness limit, not an accuracy bound — a self-join
672    /// never returns at all, so any window catches it and a generous one
673    /// cannot redden because the box was busy. The joining branch is the
674    /// one `an_instance_timer_joins_its_thread_on_drop` covers, and it
675    /// still holds a 250 ms bound.
676    #[test]
677    fn dropping_the_wheel_from_inside_its_own_release_callback_returns() {
678        /// Liveness limit. The fixed drop returns in microseconds; a
679        /// self-joined one never does.
680        const GIVE_UP: Duration = Duration::from_secs(10);
681
682        let timer = ReleaseTimer::new();
683        let deadline = timer.arm(Instant::now() + Duration::from_millis(50));
684        let state = Arc::new(DropTheWheelOnWake {
685            // The waker is now the only owner of the wheel.
686            wheel: Mutex::new(Some(timer)),
687            fired_on: Mutex::new(None),
688            returned: AtomicBool::new(false),
689        });
690
691        let waker = Waker::from(Arc::clone(&state));
692        let mut cx = Context::from_waker(&waker);
693        let mut cancelled = Box::pin(deadline.token().cancelled());
694        assert_eq!(
695            cancelled.as_mut().poll(&mut cx),
696            Poll::Pending,
697            "the deadline resolved on the first poll, so no waker was registered and the \
698             wheel would never be dropped from one",
699        );
700
701        let give_up = Instant::now() + GIVE_UP;
702        while !state.returned.load(AtomicOrdering::SeqCst) {
703            assert!(
704                Instant::now() < give_up,
705                "`ReleaseTimer::drop` did not return within {GIVE_UP:?}: {}",
706                match state.fired_on.lock().expect("waker state").as_deref() {
707                    Some(t) => format!("it is joining thread {t:?} from thread {t:?}"),
708                    None => "the waker never fired at all".to_owned(),
709                },
710            );
711            std::thread::sleep(Duration::from_millis(1));
712        }
713
714        assert_eq!(
715            state.fired_on.lock().expect("waker state").as_deref(),
716            Some("moqtap-release"),
717            "the wake did not run on the release thread, so this test dropped the wheel from \
718             somewhere the join was always safe and proves nothing",
719        );
720    }
721
722    /// The test that makes this module's reason for existing falsifiable.
723    /// Without it, "a dedicated wheel beats `tokio::time::sleep`" — the
724    /// claim the whole file rests on — is never checked at all.
725    ///
726    /// The sampled quantity is `lateness = observed - deadline`: the
727    /// requested 2 ms is subtracted from *both* arms before the percentile
728    /// is taken.
729    ///
730    /// The assertion is on a *median* because the tokio arm's p50 is
731    /// bounded below by the ~15.6 ms tick as a property of the timer, not
732    /// of the machine, while the wheel arm's p95 rises under load.
733    ///
734    /// It runs by default, unlike the two calibration measurements at the
735    /// bottom of this module, because it samples the wheel alone on a
736    /// four-worker runtime — one arm, one token, one wake — rather than the
737    /// whole read-hook-queue-release-write pipeline on a current-thread
738    /// runtime that the same load starves. This test must never be
739    /// `#[ignore]`d: it is the one the multi-threaded-runtime requirement
740    /// rests on. If a runner ever deschedules the release thread past
741    /// 2.79 ms more than half the time, weaken the factor to `x2` rather
742    /// than delete or ignore the test.
743    #[cfg(windows)]
744    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
745    async fn the_wheel_beats_the_tokio_timer_on_lateness() {
746        let timer = ReleaseTimer::new();
747        let requested = Duration::from_millis(2);
748        let mut wheel: Vec<Duration> = Vec::with_capacity(100);
749        let mut tokio_timer: Vec<Duration> = Vec::with_capacity(100);
750
751        for _ in 0..100 {
752            let at = Instant::now() + requested;
753            let d = timer.arm(at);
754            d.token().cancelled().await;
755            wheel.push(Instant::now().saturating_duration_since(at));
756
757            let at = Instant::now() + requested;
758            tokio::time::sleep(requested).await;
759            tokio_timer.push(Instant::now().saturating_duration_since(at));
760        }
761
762        let (wheel_p50, ..) = quantiles(&mut wheel);
763        let (tokio_p50, ..) = quantiles(&mut tokio_timer);
764        assert!(
765            wheel_p50 * 4 < tokio_p50,
766            "wheel p50 lateness {wheel_p50:?}, tokio p50 lateness {tokio_p50:?}"
767        );
768    }
769
770    /// Pure: no clock read inside the assertions, no thread.
771    ///
772    /// `t0 + 1 ms` is written on the *now* side rather than `t0 - 1 ms` on
773    /// the deadline side because `Instant - Duration` panics near the
774    /// clock's origin.
775    #[test]
776    fn the_wait_plan_never_overshoots() {
777        let t0 = Instant::now();
778        assert_eq!(plan(t0, None, SLICE), Wait::Idle);
779        assert_eq!(plan(t0, Some(t0), SLICE), Wait::Fire);
780        assert_eq!(plan(t0 + Duration::from_millis(1), Some(t0), SLICE), Wait::Fire,);
781        // The *nearest deadline is already inside one slice* case, served by
782        // the `min` and not by a branch.
783        assert_eq!(
784            plan(t0, Some(t0 + Duration::from_micros(100)), SLICE),
785            Wait::Sleep(Duration::from_micros(100)),
786        );
787        assert_eq!(plan(t0, Some(t0 + Duration::from_millis(5)), SLICE), Wait::Sleep(SLICE),);
788    }
789
790    /// `Slot`'s `Ord` is hand-written (`Weak` is neither `Ord` nor
791    /// `Eq`) and wrapped in `Reverse`, so one inverted comparison turns
792    /// the wheel into a LIFO that serves the farthest deadline first — a
793    /// defect that would otherwise show up only as a confusing timing
794    /// failure.
795    #[test]
796    fn the_heap_is_a_min_heap_and_ties_break_by_registration() {
797        let t0 = Instant::now();
798        let ms = |n| t0 + Duration::from_millis(n);
799
800        let mut heap: BinaryHeap<Reverse<Slot>> = BinaryHeap::new();
801        for (at, seq) in [(ms(3), 1u64), (ms(1), 2), (ms(2), 3), (ms(1), 4), (ms(1), 5)] {
802            heap.push(Reverse(Slot { at, seq, entry: Weak::new() }));
803        }
804
805        let mut popped: Vec<(Instant, u64)> = Vec::new();
806        while let Some(Reverse(s)) = heap.pop() {
807            popped.push((s.at, s.seq));
808        }
809
810        assert!(
811            popped.windows(2).all(|w| w[0].0 <= w[1].0),
812            "instants did not come out ascending: {popped:?}"
813        );
814        assert_eq!(
815            popped,
816            vec![(ms(1), 2), (ms(1), 4), (ms(1), 5), (ms(2), 3), (ms(3), 1)],
817            "ties did not break by registration order"
818        );
819    }
820
821    /// **Nothing armed is lost.** Every deadline the wheel
822    /// accepts is eventually released, none of them early, and the sweep
823    /// does not eat a live one on its way past.
824    /// This is the completeness half of what the wheel guarantees, and it is
825    /// stated the way it is *because* it has to survive a saturated box —
826    /// unlike the calibration measurements at the bottom of this module. A
827    /// descheduled release thread makes every sample **later**; it cannot make
828    /// one vanish, and it cannot make one early. So the two assertions here are
829    /// "it arrived at all" (with a give-up window 28x the deadline, which is a
830    /// liveness limit and not an accuracy bound) and *it did not arrive before
831    /// its instant*. Neither has a scheduler-dependent number in it.
832    ///
833    /// The shape is chosen so the sweep really runs **while live slots are
834    /// in the heap**, which is the case every other test here misses:
835    /// `a_deadline_never_fires_early` arms one at a time, so the heap never
836    /// reaches [`SWEEP_MIN_LEN`];
837    /// `the_release_thread_parks_when_the_wheel_is_empty`'s hundred slots
838    /// are due 5 ms after construction, long before [`SWEEP_INTERVAL`]
839    /// elapses; and `abandoned_far_deadlines_are_swept`'s ten thousand slots
840    /// are all abandoned, so a sweep that dropped live ones too would look
841    /// identical. Here 128 live and 128 abandoned slots share a heap, the
842    /// deadlines are ~700 ms out so `SWEEP_INTERVAL` passes twice first,
843    /// and the [`Parker::SleepSlice`] thread is awake every [`SLICE`]
844    /// throughout, so the rebuild is guaranteed to execute over a heap
845    /// that is half live.
846    ///
847    /// Arming runs farthest-first, so every later arm is *nearer* than the
848    /// current head and the awaits then run in deadline order.
849    #[tokio::test]
850    async fn every_armed_deadline_is_eventually_released() {
851        const LIVE: usize = 128;
852        /// Interleaved with the live ones so a sweep cannot pass over a
853        /// contiguous block of dead slots and miss the live ones.
854        const ABANDONED: usize = 128;
855        /// Far enough out that `SWEEP_INTERVAL` elapses twice before the
856        /// first slot is due.
857        const BASE: Duration = Duration::from_millis(700);
858        /// Spread, so the batch is many wakes rather than one.
859        const STEP: Duration = Duration::from_micros(200);
860        /// A liveness limit, not an accuracy bound: a lost slot never
861        /// arrives at all, so any window catches it and a generous one
862        /// cannot go red because the box was busy.
863        const GIVE_UP: Duration = Duration::from_secs(20);
864
865        let timer = ReleaseTimer::new();
866        let base = Instant::now() + BASE;
867
868        // Farthest first: every subsequent arm is nearer than the head.
869        let mut armed: Vec<(Instant, Deadline)> = Vec::with_capacity(LIVE);
870        for i in 0..LIVE {
871            let at = base + STEP * u32::try_from(LIVE - 1 - i).expect("LIVE fits in u32");
872            armed.push((at, timer.arm(at)));
873            if i < ABANDONED {
874                // Dropped immediately: a slot the wheel must reap, sitting
875                // between two it must not.
876                drop(timer.arm(at + STEP / 2));
877            }
878        }
879        assert!(
880            timer.debug_len() > SWEEP_MIN_LEN,
881            "the heap never reached the length that lets the sweep run, so this test would \
882             pass with or without one: {} slots",
883            timer.debug_len(),
884        );
885
886        // In deadline order, so each await really waits for its own slot.
887        for (i, (at, d)) in armed.iter().rev().enumerate() {
888            tokio::time::timeout(GIVE_UP, d.token().cancelled()).await.unwrap_or_else(|_| {
889                panic!(
890                    "deadline {i} of {LIVE} was armed and never released: {GIVE_UP:?} after a \
891                     deadline {BASE:?} out it is still pending, with {} slot(s) in the heap",
892                    timer.debug_len(),
893                )
894            });
895            let observed = Instant::now();
896            assert!(
897                observed >= *at,
898                "deadline {i} of {LIVE} fired {:?} early",
899                at.saturating_duration_since(observed),
900            );
901        }
902
903        assert_eq!(
904            timer.debug_len(),
905            0,
906            "every slot was popped or swept, so the heap must be empty",
907        );
908    }
909
910    /// Keeps the two process-wide read-backs compiled and exercised.
911    ///
912    /// Not a property test like the others here: `started()` and
913    /// `backend()` are thin readers
914    /// over `instrument`'s single monotonic atomic, and until `egress.rs`
915    /// and `session.rs` call them they would otherwise be `dead_code`
916    /// under `-D warnings`. Asserted in the monotone-safe direction only
917    /// (`Some` backend implies started), because nothing in this binary
918    /// constructs the process-wide wheel and a future test that does must
919    /// not turn this into a race.
920    #[test]
921    fn the_process_wide_read_backs_agree() {
922        if backend().is_some() {
923            assert!(started(), "a backend was published without `started`");
924        }
925    }
926
927    // ── calibration: opt-in ────────────────────────────────────────────
928    //
929    // The tests above are correctness properties — never early, never
930    // lost, never reordered, never left parked — and hold on a loaded box,
931    // because a starved release thread can only make a release later. What
932    // follows measures how late, and under CPU saturation that is a property
933    // of the machine as much as of the wheel: the wheel's lateness rises to
934    // the tens of milliseconds `tokio::time::sleep` costs. So these are
935    // `#[ignore]`d; run them on an otherwise idle machine:
936    //
937    // ```text
938    // cargo test -p moqtap-proxy --lib -- --ignored --nocapture calibration
939    // ```
940    //
941    // The default run still rejects the `tokio::time::sleep` implementation
942    // this module replaces, through
943    // `an_earlier_deadline_registered_later_preempts_the_current_wait` and
944    // `a_deadline_never_fires_early`. The session-level companion is
945    // `actions_timing::calibration_a_five_millisecond_delay_is_measured_as_five_milliseconds`.
946
947    /// **Calibration measurement, not a gate. Requires a quiet machine.**
948    /// Ignored by default; run with `-- --ignored --nocapture`.
949    ///
950    /// The standing proxy for a full calibration run, and the first
951    /// measurement of the Unix backends. 200 deadlines at 3 ms on one
952    /// instance; `p50 <= 5 ms` when
953    /// the backend is high-resolution, `<= 60 ms` otherwise.
954    #[tokio::test]
955    #[ignore = "calibration measurement: asserts timing accuracy, which is not \
956                measurable on a loaded box — run with --ignored on a quiet machine"]
957    async fn calibration_release_error_p50_is_within_the_platform_budget() {
958        let timer = ReleaseTimer::new();
959        let mut lateness: Vec<Duration> = Vec::with_capacity(200);
960        for _ in 0..200 {
961            let at = Instant::now() + Duration::from_millis(3);
962            let d = timer.arm(at);
963            d.token().cancelled().await;
964            lateness.push(Instant::now().saturating_duration_since(at));
965        }
966        assert_eq!(lateness.len(), 200, "lost samples");
967
968        let bound = if timer.backend().is_high_resolution() {
969            Duration::from_millis(5)
970        } else {
971            Duration::from_millis(60)
972        };
973        let (p50, p95, p99, max) = quantiles(&mut lateness);
974        // Printed as well as asserted: a calibration run is worth reading
975        // even when it is green, and `--nocapture` is how it is invoked.
976        println!(
977            "release lateness, backend {}, 200 deadlines at 3 ms: p50 {p50:?}, p95 {p95:?}, \
978             p99 {p99:?}, max {max:?} (bound {bound:?})",
979            timer.backend().as_str(),
980        );
981        assert!(
982            p50 <= bound,
983            "backend {} p50 {p50:?} exceeds {bound:?} (p95 {p95:?}, p99 {p99:?}, max {max:?})",
984            timer.backend().as_str()
985        );
986    }
987}