kunobi-daemon 0.10.0

Process ownership, request draining, and verified upgrades for local daemons
Documentation
//! A check repeated with a growing pause, where the OS offers no event.
//!
//! Three places need it: a `local::ProcessHandle` that can only follow a PID,
//! a child this process owns whose exit cannot be observed as an event, which
//! is asked through `try_wait` instead, and a wait for a `ProcessLock`, whose
//! release the OS does not signal.

use std::io;
use std::time::{Duration, Instant};

/// The first pause, doubled after each check up to [`LONGEST`].
pub(crate) const FIRST: Duration = Duration::from_millis(1);
/// The longest pause between two checks.
const LONGEST: Duration = Duration::from_millis(50);

/// Check `done` until it holds or `deadline` passes, or without a deadline.
/// True once it held. The first check is made even if the deadline has
/// passed; later checks are made only before it.
pub(crate) fn poll(
    mut done: impl FnMut() -> io::Result<bool>,
    deadline: Option<Instant>,
) -> io::Result<bool> {
    let mut pause = FIRST;
    loop {
        if done()? {
            return Ok(true);
        }
        let Some(nap) = next_pause(&mut pause, deadline) else {
            return Ok(false);
        };
        #[expect(
            clippy::disallowed_methods,
            reason = "The OS signals neither this exit nor a lock release; bound polling with backoff."
        )]
        std::thread::sleep(nap);
        // A pause may overrun. Success after the deadline is late, as in
        // `readiness::wait_until`, so it is not checked for.
        if deadline
            .is_some_and(|deadline| deadline.saturating_duration_since(Instant::now()).is_zero())
        {
            return Ok(false);
        }
    }
}

/// How long to pause before the next check, or `None` once `deadline` has
/// passed. Each pause doubles the next, up to [`LONGEST`].
pub(crate) fn next_pause(pause: &mut Duration, deadline: Option<Instant>) -> Option<Duration> {
    let nap = match deadline {
        Some(deadline) => {
            let left = deadline.saturating_duration_since(Instant::now());
            if left.is_zero() {
                return None;
            }
            left.min(*pause)
        }
        None => *pause,
    };
    *pause = LONGEST.min(*pause * 2);
    Some(nap)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn pauses_grow_up_to_their_limit_and_stop_at_the_deadline() {
        let mut pause = FIRST;
        let naps: Vec<u64> = (0..8)
            .map(|_| next_pause(&mut pause, None).unwrap().as_millis() as u64)
            .collect();
        assert_eq!(naps, [1, 2, 4, 8, 16, 32, 50, 50]);

        let mut pause = LONGEST;
        assert_eq!(next_pause(&mut pause, Some(Instant::now())), None);
        let later = Instant::now() + Duration::from_secs(10);
        assert_eq!(next_pause(&mut pause, Some(later)), Some(LONGEST));
        // A pause never runs past the deadline. A runner that stalls for the
        // whole 40 ms gets `None`, which is also right.
        let soon = Instant::now() + Duration::from_millis(40);
        if let Some(nap) = next_pause(&mut pause, Some(soon)) {
            assert!(nap > Duration::ZERO && nap <= Duration::from_millis(40));
        }
    }

    #[test]
    fn polling_ends_when_the_check_holds_or_the_deadline_passes() {
        let mut checks = 0;
        let done = poll(
            || {
                checks += 1;
                Ok(checks == 3)
            },
            None,
        );
        assert!(done.unwrap());
        assert_eq!(checks, 3);

        let deadline = Instant::now() + Duration::from_millis(20);
        assert!(!poll(|| Ok(false), Some(deadline)).unwrap());
        assert!(Instant::now() >= deadline);

        // A check that would only hold after the deadline is not made: the
        // last pause ends at the deadline, and success then would be late.
        let deadline = Instant::now() + Duration::from_millis(20);
        assert!(!poll(|| Ok(Instant::now() >= deadline), Some(deadline)).unwrap());

        let error = poll(|| Err(io::Error::other("check failed")), None).unwrap_err();
        assert_eq!(error.to_string(), "check failed");
    }
}