syd 3.52.0

rock-solid application kernel
Documentation
//
// Syd: rock-solid application kernel
// src/timer.rs: Per-thread SIGALRM timer
//
// Copyright (c) 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{mem, time::Duration};

use nix::{errno::Errno, sys::signal::Signal, time::ClockId, unistd::gettid};

use crate::{
    compat::{Expiration, TimerSetTimeFlags},
    fs::{block_signal, unblock_signal},
};

/// Per-thread SIGALRM timer that can be armed to interrupt a blocking syscall
/// in this *thread* after `keep_alive` nanoseconds.
///
/// Linux-only through SIGEV_THREAD_ID.
pub struct AlarmTimer {
    keep_alive: u64,
    timer_id: libc::timer_t,
}

impl AlarmTimer {
    /// Create a per-thread timer targeted at the *current* thread.
    /// `keep_alive` is in seconds; if 0, `start()` will be a no-op.
    pub fn from_seconds(keep_alive: u64) -> Result<Self, Errno> {
        Self::from_nanoseconds(keep_alive.checked_mul(1_000_000_000).ok_or(Errno::ERANGE)?)
    }

    /// Create a per-thread timer targeted at the *current* thread.
    /// `keep_alive` is in milliseconds; if 0, `start()` will be a no-op.
    pub fn from_milliseconds(keep_alive: u64) -> Result<Self, Errno> {
        Self::from_nanoseconds(keep_alive.checked_mul(1_000_000).ok_or(Errno::ERANGE)?)
    }

    /// Create a per-thread timer targeted at the *current* thread.
    /// `keep_alive` is in nanoseconds; if 0, `start()` will be a no-op.
    pub fn from_nanoseconds(keep_alive: u64) -> Result<Self, Errno> {
        // Route SIGALRM specifically to this thread (Linux extension).
        // nix does not implement SIGEV_THREAD_ID for musl yet so we use libc.
        const SIGEV_THREAD_ID: libc::c_int = 4;

        // SAFETY: sigevent is a plain C struct.
        let mut sev: libc::sigevent = unsafe { mem::zeroed() };
        sev.sigev_notify = SIGEV_THREAD_ID;
        sev.sigev_signo = libc::SIGALRM;
        sev.sigev_notify_thread_id = gettid().as_raw();

        // Monotonic clock; initially disarmed.
        let mut timer_id: mem::MaybeUninit<libc::timer_t> = mem::MaybeUninit::uninit();
        // SAFETY: `sev` is fully initialized above; `timer_id` is a valid out-pointer.
        Errno::result(unsafe {
            crate::compat::timer_create(
                ClockId::CLOCK_MONOTONIC.as_raw(),
                std::ptr::addr_of_mut!(sev),
                timer_id.as_mut_ptr(),
            )
        })
        .map(|_| {
            Self {
                keep_alive,
                // SAFETY: timer_create initializes timer_id on success.
                timer_id: unsafe { timer_id.assume_init() },
            }
        })
    }

    /// Arm the timer for `self.keep_alive` seconds (if > 0) and make sure
    /// SIGALRM is unblocked *in this thread* so it can interrupt.
    /// If `keep_alive` == 0, does nothing and returns Ok(()).
    pub fn start(&mut self) -> Result<(), Errno> {
        if self.keep_alive == 0 {
            return Ok(());
        }

        // Allow delivery to this thread while we're blocking.
        unblock_signal(Signal::SIGALRM)?;

        // One-shot expiration at keep_alive nanoseconds.
        let dur = Duration::from_nanos(self.keep_alive);
        let exp = Expiration::OneShot(dur.into());
        self.set(exp, TimerSetTimeFlags::empty())
    }

    /// Disarm the timer and re-block SIGALRM in this thread.
    /// Idempotent: safe to call even if `start()` was a no-op.
    pub fn stop(&mut self) -> Result<(), Errno> {
        // Disarm: set a one-shot with zero interval and zero value.
        let exp = Expiration::OneShot(Duration::from_secs(0).into());
        self.set(exp, TimerSetTimeFlags::empty())?;

        // Restore per-thread policy: block SIGALRM again.
        block_signal(Signal::SIGALRM)
    }

    /// Change keep-alive seconds for future `start()` calls.
    pub fn set_keep_alive(&mut self, keep_alive: u64) {
        self.keep_alive = keep_alive;
    }

    /// Read current keep-alive.
    pub fn keep_alive(&self) -> u64 {
        self.keep_alive
    }

    fn set(&mut self, expiration: Expiration, flags: TimerSetTimeFlags) -> Result<(), Errno> {
        let timerspec: TimerSpec = expiration.into();

        // SAFETY: `self.timer_id` is valid (created in `from_nanoseconds`),
        // `timerspec` is a valid reference, NULL old_value is allowed.
        Errno::result(unsafe {
            crate::compat::timer_settime(
                self.timer_id,
                flags.bits(),
                timerspec.as_ref(),
                std::ptr::null_mut(),
            )
        })
        .map(drop)
    }
}

impl Drop for AlarmTimer {
    fn drop(&mut self) {
        #[expect(clippy::disallowed_methods)]
        // SAFETY: `self.timer_id` is valid (created in `from_nanoseconds`).
        // Panics on failure because a timer_delete error indicates a
        // corrupted timer state.
        Errno::result(unsafe { crate::compat::timer_delete(self.timer_id) })
            .map(drop)
            .expect("timer_delete")
    }
}

// Rest is borrowed from nix' src/sys/time.rs
// because this type is not exported :'(.
#[expect(clippy::disallowed_types)]
const fn zero_init_timespec() -> libc::timespec {
    // SAFETY: `std::mem::MaybeUninit::zeroed()` is not yet a const fn
    // (https://github.com/rust-lang/rust/issues/91850) so we will instead initialize an array of
    // the appropriate size to zero and then transmute it to a timespec value.
    unsafe { std::mem::transmute([0u8; size_of::<libc::timespec>()]) }
}

#[derive(Debug, Clone, Copy)]
pub(crate) struct TimerSpec(libc::itimerspec);

impl AsMut<libc::itimerspec> for TimerSpec {
    fn as_mut(&mut self) -> &mut libc::itimerspec {
        &mut self.0
    }
}

impl AsRef<libc::itimerspec> for TimerSpec {
    fn as_ref(&self) -> &libc::itimerspec {
        &self.0
    }
}

impl From<Expiration> for TimerSpec {
    fn from(expiration: Expiration) -> TimerSpec {
        match expiration {
            Expiration::OneShot(t) => TimerSpec(libc::itimerspec {
                it_interval: zero_init_timespec(),
                it_value: *t.as_ref(),
            }),
            Expiration::IntervalDelayed(start, interval) => TimerSpec(libc::itimerspec {
                it_interval: *interval.as_ref(),
                it_value: *start.as_ref(),
            }),
            Expiration::Interval(t) => TimerSpec(libc::itimerspec {
                it_interval: *t.as_ref(),
                it_value: *t.as_ref(),
            }),
        }
    }
}

#[cfg(test)]
mod tests {
    use std::time::Duration;

    use super::*;

    #[test]
    fn test_timer_zero_init_timespec_1() {
        let ts = zero_init_timespec();
        assert_eq!(ts.tv_sec, 0);
        assert_eq!(ts.tv_nsec, 0);
    }

    #[test]
    fn test_timer_from_1() {
        let dur = Duration::from_secs(5);
        let ts: TimerSpec = Expiration::OneShot(dur.into()).into();
        let inner = ts.as_ref();
        assert_eq!(inner.it_value.tv_sec, 5);
        assert_eq!(inner.it_value.tv_nsec, 0);
        assert_eq!(inner.it_interval.tv_sec, 0);
        assert_eq!(inner.it_interval.tv_nsec, 0);
    }

    #[test]
    fn test_timer_from_2() {
        let dur = Duration::from_millis(500);
        let ts: TimerSpec = Expiration::Interval(dur.into()).into();
        let inner = ts.as_ref();
        assert_eq!(inner.it_value.tv_sec, 0);
        assert_eq!(inner.it_value.tv_nsec, 500_000_000);
        assert_eq!(inner.it_interval.tv_sec, 0);
        assert_eq!(inner.it_interval.tv_nsec, 500_000_000);
    }

    #[test]
    fn test_timer_from_3() {
        let start = Duration::from_secs(1);
        let interval = Duration::from_secs(2);
        let ts: TimerSpec = Expiration::IntervalDelayed(start.into(), interval.into()).into();
        let inner = ts.as_ref();
        assert_eq!(inner.it_value.tv_sec, 1);
        assert_eq!(inner.it_interval.tv_sec, 2);
    }

    #[test]
    fn test_timer_as_mut_1() {
        let dur = Duration::from_secs(1);
        let mut ts: TimerSpec = Expiration::OneShot(dur.into()).into();
        let inner = ts.as_mut();
        inner.it_value.tv_sec = 42;
        assert_eq!(ts.as_ref().it_value.tv_sec, 42);
    }

    #[test]
    fn test_timer_from_nanoseconds_1() {
        let timer = AlarmTimer::from_nanoseconds(0).unwrap();
        assert_eq!(timer.keep_alive(), 0);
    }

    #[test]
    fn test_timer_from_seconds_1() {
        let timer = AlarmTimer::from_seconds(1).unwrap();
        assert_eq!(timer.keep_alive(), 1_000_000_000);
    }

    #[test]
    fn test_timer_from_milliseconds_1() {
        let timer = AlarmTimer::from_milliseconds(500).unwrap();
        assert_eq!(timer.keep_alive(), 500_000_000);
    }

    #[test]
    fn test_timer_from_seconds_2() {
        match AlarmTimer::from_seconds(u64::MAX) {
            Err(e) => assert_eq!(e, nix::errno::Errno::ERANGE),
            Ok(_) => panic!("expected ERANGE"),
        }
    }

    #[test]
    fn test_timer_from_milliseconds_2() {
        match AlarmTimer::from_milliseconds(u64::MAX) {
            Err(e) => assert_eq!(e, nix::errno::Errno::ERANGE),
            Ok(_) => panic!("expected ERANGE"),
        }
    }

    #[test]
    fn test_timer_set_keep_alive_1() {
        let mut timer = AlarmTimer::from_nanoseconds(100).unwrap();
        assert_eq!(timer.keep_alive(), 100);
        timer.set_keep_alive(200);
        assert_eq!(timer.keep_alive(), 200);
    }

    #[test]
    fn test_timer_start_1() {
        let mut timer = AlarmTimer::from_nanoseconds(0).unwrap();
        assert!(timer.start().is_ok());
        assert!(timer.stop().is_ok());
    }

    #[test]
    fn test_timer_start_2() {
        let mut timer = AlarmTimer::from_seconds(10).unwrap();
        assert!(timer.start().is_ok());
        assert!(timer.stop().is_ok());
    }

    #[test]
    fn test_timer_drop_1() {
        let timer = AlarmTimer::from_nanoseconds(1_000_000).unwrap();
        drop(timer);
    }
}