Chapter 10 of 17

Time

Sleeps to the millisecond or the microsecond, timeouts and intervals, and a clock that tests can stop.

Sleeps and timeouts

src/main.rs
use std::time::Duration;

use nuclei::time::{self, Instant};

#[nuclei::main]
async fn main() {
    let start = Instant::now();
    time::sleep(Duration::from_millis(20)).await;
    assert!(start.elapsed() >= Duration::from_millis(20));

    // A future that takes too long gives Elapsed.
    let slow = time::sleep(Duration::from_secs(5));
    assert!(
        time::timeout(Duration::from_millis(10), slow)
            .await
            .is_err()
    );

    // One that is ready in time gives its output.
    let quick = async { "in time" };
    assert_eq!(
        time::timeout(Duration::from_secs(1), quick).await,
        Ok("in time")
    );

    // The same against a deadline.
    let deadline = Instant::now() + Duration::from_millis(10);
    let never = std::future::pending::<()>();
    assert!(time::timeout_at(deadline, never).await.is_err());
}
  • sleep(duration) and sleep_until(instant) give a Sleep; reset moves its deadline, deadline and is_elapsed read it.
  • timeout(duration, future) and timeout_at(deadline, future) give Ok(output), or Err(Elapsed) if the time ran out first. The future is polled before the clock is read, so one that is ready at once always wins.
  • Dropping a timer's future cancels it.

The clock and the wheel

Every shard has a timer wheel with millisecond slots, where sleeps, timeouts and intervals wait. Such a timer waits at least its duration, rounded up to the next millisecond, and fires on the shard that polls it; a shard busy with a task fires it once that task yields. Arming and cancelling take constant time. A timer polled on a thread that runs no shard (another runtime's) is fired for it by a shard of the current runtime, or of the default one. A sleep that needs a finer step waits to the microsecond.

nuclei::time::Instant is the runtime's clock. It reads the system's monotonic clock until it is paused.

Sleeps to the microsecond

sleep_precise(duration) and sleep_until_precise(instant) give a SleepPrecise, which ends at its deadline to the microsecond. Its timer sits in the shard's microsecond wheel, and the shard's wait ends at the earliest deadline of both wheels through the backend's high-resolution timer: the wait's own timeout on io_uring and on Windows, a timerfd on epoll, and an EVFILT_TIMER in nanoseconds on kqueue.

src/main.rs
use std::time::Duration;

use nuclei::time::{self, Instant};

#[nuclei::main]
async fn main() {
    // To the microsecond, where sleep would round up to the next millisecond.
    let start = Instant::now();
    time::sleep_precise(Duration::from_micros(250)).await;
    assert!(start.elapsed() >= Duration::from_micros(250));

    // A pacer: ten steps 100 microseconds apart, each deadline counted from
    // the last one, so a late wake does not push the rest back.
    let begin = Instant::now();
    let mut next = begin;
    for _ in 0..10 {
        next += Duration::from_micros(100);
        time::sleep_until_precise(next).await;
    }
    assert!(begin.elapsed() >= Duration::from_millis(1));
}
  • It waits at least its duration, rounded up to the microsecond, and ends as soon after that as the system's timer wakes the shard. A shard busy with a task runs it late by as long as that task takes to yield, as with any timer.
  • A precise deadline that is the shard's earliest costs the shard a timer of its own in the kernel, one more system call to arm on epoll. Sleeps on the millisecond wheel pay nothing for it, so keep sleep where a millisecond is fine.
  • Pick it for a pacer, a retry spaced in microseconds, or a deadline a protocol counts in microseconds.
  • deadline, is_elapsed and reset work as on a Sleep, and a paused clock moves to a precise deadline exactly.

Intervals

interval(period) ticks every period, the first tick at once; interval_at(start, period) starts at start. tick().await gives the instant the tick was due, and poll_tick is the same wait for a hand-written future. A period of zero panics.

src/main.rs
use std::time::Duration;

use nuclei::time::{self, MissedTickBehavior};

#[nuclei::main]
async fn main() {
    let mut every = time::interval(Duration::from_millis(10));
    // When polled late, skip the ticks that were missed instead of bursting them.
    every.set_missed_tick_behavior(MissedTickBehavior::Skip);

    let first = every.tick().await; // the first tick is at once
    for _ in 0..3 {
        every.tick().await;
    }
    assert!(first.elapsed() >= Duration::from_millis(30));

    // reset: the next tick is one period from now.
    every.reset();
    every.tick().await;
    assert_eq!(every.period(), Duration::from_millis(10));
}
MissedTickBehaviorWhen the interval was polled late
Burst (the default)Every missed tick fires at once, one after another, until the interval is back on its schedule.
DelayThe next tick comes one period after the late one: the schedule moves.
SkipMissed ticks are skipped: the next tick is the next one on the original schedule after now.

Paused time

Code that sleeps is slow to test. A paused clock stands still and moves only when asked, so an hour's timeout passes at once and the test still sees an hour on the clock.

  • time::pause() stops the clock of the runtime the code runs in. Builder::start_paused(true), #[nuclei::main(start_paused = true)] and #[nuclei::test(start_paused = true)] start it stopped.
  • It moves by itself when nothing else can. Once every shard of the runtime is idle with a timer armed, the clock jumps to the earliest timer.
  • time::advance(duration).await moves it by hand and lets the timers that came due fire before it returns.
  • time::resume() starts it again from where it stands.
src/main.rs
use std::time::Duration;

use nuclei::time::{self, Instant};

#[nuclei::main(start_paused = true)]
async fn main() {
    let start = Instant::now();

    // Paused, the clock jumps to the next timer once every task waits:
    // an hour's sleep ends at once, and the clock says an hour passed.
    time::sleep(Duration::from_secs(3600)).await;
    assert_eq!(start.elapsed(), Duration::from_secs(3600));

    // advance moves the clock by hand, firing the timers it passes.
    let timer = nuclei::spawn(time::sleep(Duration::from_secs(10)));
    time::advance(Duration::from_secs(10)).await;
    timer.await;
    assert_eq!(start.elapsed(), Duration::from_secs(3610));

    // resume lets it run with the system's clock again.
    time::resume();
    time::sleep(Duration::from_millis(1)).await;
    assert!(start.elapsed() > Duration::from_secs(3610));
}

A paused clock never goes backwards, on any thread, across a pause, a resume or an advance. While no runtime's clock is paused or advanced, reading it costs one load more than reading the system clock.