soft-cycle 0.2.0

Async controller for coordinating soft restarts and graceful shutdowns with shared listeners
Documentation
//! Global [`SoftCycleController`] instance and convenience functions.
//!
//! This module is only available when the `global_instance` feature is enabled (default).

use std::sync::atomic::AtomicU8;

use tokio::sync::OnceCell;

use crate::{Payload, SoftCycleController, SoftCycleListener};

/// Message type for the global controller.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SoftCycleMessage {
    Shutdown,
    Restart,
}

/// Converts a `u8` to `SoftCycleMessage`.
///
/// **Valid values:** `0` → [`Shutdown`](Self::Shutdown), `1` → [`Restart`](Self::Restart).
///
/// **Invalid values:** Any other `u8` (e.g. `2..=255`) causes a **panic**. Use this conversion
/// only with values produced by [`Into::into`](Into) from a `SoftCycleMessage`, or when the
/// value is otherwise known to be 0 or 1.
impl From<u8> for SoftCycleMessage {
    fn from(value: u8) -> Self {
        match value {
            0 => Self::Shutdown,
            1 => Self::Restart,
            _ => panic!("Invalid soft cycle message: {}", value),
        }
    }
}

impl From<SoftCycleMessage> for u8 {
    fn from(value: SoftCycleMessage) -> Self {
        match value {
            SoftCycleMessage::Shutdown => 0,
            SoftCycleMessage::Restart => 1,
        }
    }
}

impl Payload for SoftCycleMessage {
    type UnderlyingAtomic = AtomicU8;
}

/// Global [`SoftCycleController`] instance, lazily initialized on first use.
static SHUTDOWN_CONTROLLER: OnceCell<SoftCycleController<SoftCycleMessage>> = OnceCell::const_new();

/// Returns a reference to the global [`SoftCycleController`], initializing it on first call.
pub async fn get_lifetime_controller() -> &'static SoftCycleController<SoftCycleMessage> {
    SHUTDOWN_CONTROLLER
        .get_or_init(|| async { SoftCycleController::new() })
        .await
}

/// Attempts to notify a shutdown on the global controller.
///
/// Returns `true` if the notify succeeded, `false` if already notified.
///
/// Equivalent to calling
/// [`SoftCycleController::try_notify`](crate::SoftCycleController::try_notify)([`SoftCycleMessage::Shutdown`]) on the
/// global instance.
#[must_use = "Caller must check if the operation was successful"]
pub async fn try_shutdown() -> bool {
    get_lifetime_controller()
        .await
        .try_notify(SoftCycleMessage::Shutdown)
        .is_ok()
}

/// Attempts to notify a restart on the global controller.
///
/// Returns `true` if the notify succeeded, `false` if already notified.
///
/// Equivalent to calling
/// [`SoftCycleController::try_notify`](crate::SoftCycleController::try_notify)([`SoftCycleMessage::Restart`]) on the
/// global instance.
#[must_use = "Caller must check if the operation was successful"]
pub async fn try_restart() -> bool {
    get_lifetime_controller()
        .await
        .try_notify(SoftCycleMessage::Restart)
        .is_ok()
}

/// Listener on the global controller.
///
/// Resolves with `Ok(SoftCycleMessage::Shutdown)` for shutdown and
/// `Ok(SoftCycleMessage::Restart)` for restart.
///
/// Equivalent to calling `get_lifetime_controller().await.listener()`.
#[must_use = "Caller must await the listener to receive the signal"]
pub async fn listener() -> SoftCycleListener<'static, SoftCycleMessage> {
    get_lifetime_controller().await.listener()
}

/// Clears the notified state on the global controller.
///
/// This convenience function discards the return value; callers that need the
/// cleared sequence number must use
/// `get_lifetime_controller().await.try_clear()` instead.
pub async fn clear() {
    let _ = get_lifetime_controller().await.try_clear();
}

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

    // --- Conversion boundaries: valid values ---

    #[test]
    fn conversion_u8_to_message_valid_zero_is_shutdown() {
        let m: SoftCycleMessage = 0u8.into();
        assert_eq!(m, SoftCycleMessage::Shutdown);
    }

    #[test]
    fn conversion_u8_to_message_valid_one_is_restart() {
        let m: SoftCycleMessage = 1u8.into();
        assert_eq!(m, SoftCycleMessage::Restart);
    }

    #[test]
    fn conversion_message_to_u8_roundtrip() {
        assert_eq!(u8::from(SoftCycleMessage::Shutdown), 0);
        assert_eq!(u8::from(SoftCycleMessage::Restart), 1);
        assert_eq!(
            SoftCycleMessage::from(u8::from(SoftCycleMessage::Shutdown)),
            SoftCycleMessage::Shutdown
        );
        assert_eq!(
            SoftCycleMessage::from(u8::from(SoftCycleMessage::Restart)),
            SoftCycleMessage::Restart
        );
    }

    // --- Conversion boundaries: invalid values panic ---

    #[test]
    #[should_panic(expected = "Invalid soft cycle message")]
    fn conversion_u8_to_message_invalid_panics_two() {
        let _: SoftCycleMessage = 2u8.into();
    }

    #[test]
    #[should_panic(expected = "Invalid soft cycle message")]
    fn conversion_u8_to_message_invalid_panics_255() {
        let _: SoftCycleMessage = 255u8.into();
    }
}