Skip to main content

soft_cycle/
global.rs

1//! Global [`SoftCycleController`] instance and convenience functions.
2//!
3//! This module is only available when the `global_instance` feature is enabled (default).
4
5use std::sync::atomic::AtomicU8;
6
7use tokio::sync::OnceCell;
8
9use crate::{Payload, SoftCycleController, SoftCycleListener};
10
11/// Message type for the global controller.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum SoftCycleMessage {
14    Shutdown,
15    Restart,
16}
17
18/// Converts a `u8` to `SoftCycleMessage`.
19///
20/// **Valid values:** `0` → [`Shutdown`](Self::Shutdown), `1` → [`Restart`](Self::Restart).
21///
22/// **Invalid values:** Any other `u8` (e.g. `2..=255`) causes a **panic**. Use this conversion
23/// only with values produced by [`Into::into`](Into) from a `SoftCycleMessage`, or when the
24/// value is otherwise known to be 0 or 1.
25impl From<u8> for SoftCycleMessage {
26    fn from(value: u8) -> Self {
27        match value {
28            0 => Self::Shutdown,
29            1 => Self::Restart,
30            _ => panic!("Invalid soft cycle message: {}", value),
31        }
32    }
33}
34
35impl From<SoftCycleMessage> for u8 {
36    fn from(value: SoftCycleMessage) -> Self {
37        match value {
38            SoftCycleMessage::Shutdown => 0,
39            SoftCycleMessage::Restart => 1,
40        }
41    }
42}
43
44impl Payload for SoftCycleMessage {
45    type UnderlyingAtomic = AtomicU8;
46}
47
48/// Global [`SoftCycleController`] instance, lazily initialized on first use.
49static SHUTDOWN_CONTROLLER: OnceCell<SoftCycleController<SoftCycleMessage>> = OnceCell::const_new();
50
51/// Returns a reference to the global [`SoftCycleController`], initializing it on first call.
52pub async fn get_lifetime_controller() -> &'static SoftCycleController<SoftCycleMessage> {
53    SHUTDOWN_CONTROLLER
54        .get_or_init(|| async { SoftCycleController::new() })
55        .await
56}
57
58/// Attempts to notify a shutdown on the global controller.
59///
60/// Returns `true` if the notify succeeded, `false` if already notified.
61///
62/// Equivalent to calling
63/// [`SoftCycleController::try_notify`](crate::SoftCycleController::try_notify)([`SoftCycleMessage::Shutdown`]) on the
64/// global instance.
65#[must_use = "Caller must check if the operation was successful"]
66pub async fn try_shutdown() -> bool {
67    get_lifetime_controller()
68        .await
69        .try_notify(SoftCycleMessage::Shutdown)
70        .is_ok()
71}
72
73/// Attempts to notify a restart on the global controller.
74///
75/// Returns `true` if the notify succeeded, `false` if already notified.
76///
77/// Equivalent to calling
78/// [`SoftCycleController::try_notify`](crate::SoftCycleController::try_notify)([`SoftCycleMessage::Restart`]) on the
79/// global instance.
80#[must_use = "Caller must check if the operation was successful"]
81pub async fn try_restart() -> bool {
82    get_lifetime_controller()
83        .await
84        .try_notify(SoftCycleMessage::Restart)
85        .is_ok()
86}
87
88/// Listener on the global controller.
89///
90/// Resolves with `Ok(SoftCycleMessage::Shutdown)` for shutdown and
91/// `Ok(SoftCycleMessage::Restart)` for restart.
92///
93/// Equivalent to calling `get_lifetime_controller().await.listener()`.
94#[must_use = "Caller must await the listener to receive the signal"]
95pub async fn listener() -> SoftCycleListener<'static, SoftCycleMessage> {
96    get_lifetime_controller().await.listener()
97}
98
99/// Clears the notified state on the global controller.
100///
101/// This convenience function discards the return value; callers that need the
102/// cleared sequence number must use
103/// `get_lifetime_controller().await.try_clear()` instead.
104pub async fn clear() {
105    let _ = get_lifetime_controller().await.try_clear();
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    // --- Conversion boundaries: valid values ---
113
114    #[test]
115    fn conversion_u8_to_message_valid_zero_is_shutdown() {
116        let m: SoftCycleMessage = 0u8.into();
117        assert_eq!(m, SoftCycleMessage::Shutdown);
118    }
119
120    #[test]
121    fn conversion_u8_to_message_valid_one_is_restart() {
122        let m: SoftCycleMessage = 1u8.into();
123        assert_eq!(m, SoftCycleMessage::Restart);
124    }
125
126    #[test]
127    fn conversion_message_to_u8_roundtrip() {
128        assert_eq!(u8::from(SoftCycleMessage::Shutdown), 0);
129        assert_eq!(u8::from(SoftCycleMessage::Restart), 1);
130        assert_eq!(
131            SoftCycleMessage::from(u8::from(SoftCycleMessage::Shutdown)),
132            SoftCycleMessage::Shutdown
133        );
134        assert_eq!(
135            SoftCycleMessage::from(u8::from(SoftCycleMessage::Restart)),
136            SoftCycleMessage::Restart
137        );
138    }
139
140    // --- Conversion boundaries: invalid values panic ---
141
142    #[test]
143    #[should_panic(expected = "Invalid soft cycle message")]
144    fn conversion_u8_to_message_invalid_panics_two() {
145        let _: SoftCycleMessage = 2u8.into();
146    }
147
148    #[test]
149    #[should_panic(expected = "Invalid soft cycle message")]
150    fn conversion_u8_to_message_invalid_panics_255() {
151        let _: SoftCycleMessage = 255u8.into();
152    }
153}