Ctrl K
ComunidadGuías…

src/systemd.rs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
//! Thin wrapper around systemd `sd_notify` integration.
//!
//! All functions are no-ops when `enabled` is false or on non-Linux platforms.
//! This keeps the rest of the codebase free from `#[cfg]` conditionals.

use std::time::Duration;

use tokio_util::sync::CancellationToken;

/// Holds the runtime flag controlling whether sd-notify messages are sent.
#[derive(Debug, Clone, Copy)]
pub(crate) struct SystemdNotifier {
    enabled: bool,
}

#[derive(Debug)]
pub(crate) struct SystemdWatchdogTask {
    handle: Option<tokio::task::JoinHandle<()>>,
}

impl Drop for SystemdWatchdogTask {
    fn drop(&mut self) {
        if let Some(handle) = self.handle.take() {
            handle.abort();
        }
    }
}

impl SystemdNotifier {
    /// Create a new notifier. When `enabled` is false, all methods are no-ops.
    pub(crate) const fn new(enabled: bool) -> Self {
        Self { enabled }
    }

    /// Send `READY=1` to systemd (service startup complete).
    pub(crate) fn notify_ready(self) {
        if !self.enabled {
            return;
        }
        self.send_impl_ready();
    }

    /// Send `STOPPING=1` to systemd (service shutting down).
    pub(crate) fn notify_stopping(self) {
        if !self.enabled {
            return;
        }
        self.send_impl_stopping();
    }

    /// Send `STATUS=<msg>` to systemd (human-readable status).
    pub(crate) fn notify_status(self, msg: &str) {
        if !self.enabled {
            return;
        }
        self.send_impl_status(msg);
    }

    /// Send `WATCHDOG=1` to systemd (keepalive ping).
    pub(crate) fn notify_watchdog(self) {
        if !self.enabled {
            return;
        }
        self.send_impl_watchdog();
    }

    /// Start a background watchdog heartbeat when systemd requested one.
    ///
    /// systemd passes `WATCHDOG_USEC` to notify services when `WatchdogSec=`
    /// is active. A long watch-mode sleep can be much larger than that
    /// timeout, so a cycle-bound ping is not enough; keep pinging until the
    /// sync run exits or shutdown starts.
    pub(crate) fn start_watchdog_heartbeat(
        self,
        shutdown_token: CancellationToken,
    ) -> SystemdWatchdogTask {
        let Some(timeout) = self.watchdog_timeout() else {
            return SystemdWatchdogTask { handle: None };
        };
        let interval = watchdog_heartbeat_interval(timeout);
        let handle = tokio::spawn(watchdog_heartbeat_loop(
            interval,
            shutdown_token,
            move || self.notify_watchdog(),
        ));
        SystemdWatchdogTask {
            handle: Some(handle),
        }
    }

    fn watchdog_timeout(self) -> Option<Duration> {
        if !self.enabled {
            return None;
        }
        self.watchdog_timeout_impl()
    }

    #[cfg(target_os = "linux")]
    fn send_impl_ready(self) {
        if let Err(e) = sd_notify::notify(&[sd_notify::NotifyState::Ready]) {
            tracing::debug!(error = %e, "sd_notify READY failed");
        }
    }

    #[cfg(not(target_os = "linux"))]
    fn send_impl_ready(self) {
        let _ = self;
    }

    #[cfg(target_os = "linux")]
    fn send_impl_stopping(self) {
        if let Err(e) = sd_notify::notify(&[sd_notify::NotifyState::Stopping]) {
            tracing::debug!(error = %e, "sd_notify STOPPING failed");
        }
    }

    #[cfg(not(target_os = "linux"))]
    fn send_impl_stopping(self) {
        let _ = self;
    }

    #[cfg(target_os = "linux")]
    fn send_impl_status(self, msg: &str) {
        if let Err(e) = sd_notify::notify(&[sd_notify::NotifyState::Status(msg)]) {
            tracing::debug!(error = %e, "sd_notify STATUS failed");
        }
    }

    #[cfg(not(target_os = "linux"))]
    fn send_impl_status(self, _msg: &str) {
        let _ = self;
    }

    #[cfg(target_os = "linux")]
    fn send_impl_watchdog(self) {
        if let Err(e) = sd_notify::notify(&[sd_notify::NotifyState::Watchdog]) {
            tracing::debug!(error = %e, "sd_notify WATCHDOG failed");
        }
    }

    #[cfg(not(target_os = "linux"))]
    fn send_impl_watchdog(self) {
        let _ = self;
    }

    #[cfg(target_os = "linux")]
    fn watchdog_timeout_impl(self) -> Option<Duration> {
        sd_notify::watchdog_enabled()
    }

    #[cfg(not(target_os = "linux"))]
    fn watchdog_timeout_impl(self) -> Option<Duration> {
        let _ = self;
        None
    }
}

fn watchdog_heartbeat_interval(timeout: Duration) -> Duration {
    if timeout.is_zero() {
        return Duration::from_secs(1);
    }
    timeout
        .checked_div(2)
        .filter(|d| !d.is_zero())
        .unwrap_or(timeout)
}

async fn watchdog_heartbeat_loop<F>(
    interval: Duration,
    shutdown_token: CancellationToken,
    mut notify_watchdog: F,
) where
    F: FnMut(),
{
    notify_watchdog();
    loop {
        tokio::select! {
            () = tokio::time::sleep(interval) => notify_watchdog(),
            () = shutdown_token.cancelled() => break,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Arc;
    use std::sync::atomic::{AtomicUsize, Ordering};

    #[test]
    fn disabled_notifier_is_noop() {
        let n = SystemdNotifier::new(false);
        n.notify_ready();
        n.notify_stopping();
        n.notify_status("test");
        n.notify_watchdog();
    }

    #[test]
    fn enabled_notifier_does_not_panic() {
        // On non-Linux this is still a no-op; on Linux without a socket it logs debug
        let n = SystemdNotifier::new(true);
        n.notify_ready();
        n.notify_stopping();
        n.notify_status("test");
        n.notify_watchdog();
    }

    #[test]
    fn watchdog_heartbeat_interval_is_half_timeout() {
        assert_eq!(
            watchdog_heartbeat_interval(Duration::from_secs(120)),
            Duration::from_secs(60)
        );
        assert_eq!(
            watchdog_heartbeat_interval(Duration::from_micros(1)),
            Duration::from_nanos(500)
        );
        assert_eq!(
            watchdog_heartbeat_interval(Duration::ZERO),
            Duration::from_secs(1)
        );
    }

    #[tokio::test(start_paused = true)]
    async fn watchdog_heartbeat_loop_pings_until_shutdown() {
        let shutdown_token = CancellationToken::new();
        let calls = Arc::new(AtomicUsize::new(0));
        let notify_calls = Arc::clone(&calls);
        let task = tokio::spawn(watchdog_heartbeat_loop(
            Duration::from_secs(60),
            shutdown_token.clone(),
            move || {
                notify_calls.fetch_add(1, Ordering::SeqCst);
            },
        ));

        tokio::task::yield_now().await;
        assert_eq!(calls.load(Ordering::SeqCst), 1);

        tokio::time::sleep(Duration::from_secs(60)).await;
        tokio::task::yield_now().await;
        assert_eq!(calls.load(Ordering::SeqCst), 2);

        shutdown_token.cancel();
        task.await.unwrap();

        tokio::time::sleep(Duration::from_secs(60)).await;
        assert_eq!(calls.load(Ordering::SeqCst), 2);
    }
}