Skip to main content

soft_cycle/
lib.rs

1#![doc = include_str!("../README.md")]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3
4use std::{
5    future::Future,
6    pin::Pin,
7    sync::{
8        Arc,
9        atomic::{AtomicBool, Ordering},
10    },
11    task::{Context, Poll},
12};
13
14use tokio::sync::{Notify, futures::OwnedNotified};
15
16/// Future that completes when the associated controller triggers a restart or shutdown.
17///
18/// Returned by [`SoftCycleController::listener`]. Resolves to `true` for shutdown, `false` for restart.
19pub struct SoftCycleListener<'a> {
20    notify: OwnedNotified,
21    controller: &'a SoftCycleController,
22}
23
24impl Future for SoftCycleListener<'_> {
25    type Output = bool;
26
27    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
28        let notify_pin = unsafe { self.as_mut().map_unchecked_mut(|s| &mut s.notify) };
29
30        notify_pin
31            .poll(cx)
32            .map(|_| self.controller.is_shutdown.load(Ordering::Relaxed))
33    }
34}
35
36/// Controller for soft restarts and graceful shutdowns.
37///
38/// See the [crate-level documentation](crate) for usage, notices, and a full example.
39pub struct SoftCycleController {
40    /// Whether a restart or shutdown has already been triggered.
41    ///
42    /// Used to prevent multiple triggers.
43    triggered: AtomicBool,
44    /// Is the triggered action a shutdown (true) or restart (false).
45    is_shutdown: AtomicBool,
46
47    /// Notifier to signal when a restart or shutdown has been triggered.
48    notify: Arc<Notify>,
49}
50
51impl SoftCycleController {
52    /// Creates a new [`SoftCycleController`].
53    #[allow(clippy::new_without_default)]
54    pub fn new() -> Self {
55        Self {
56            triggered: AtomicBool::new(false),
57            is_shutdown: AtomicBool::new(false),
58            notify: Arc::new(Notify::new()),
59        }
60    }
61
62    /// Attempts to trigger a restart.
63    ///
64    /// Returns true if successful, and false if a shutdown or restart has
65    /// already been triggered.
66    #[must_use = "Caller must check if the operation was successful"]
67    pub async fn try_restart(&self) -> bool {
68        self.try_trigger(false).await
69    }
70
71    /// Attempts to trigger a shutdown.
72    ///
73    /// Returns true if successful, and false if a shutdown or restart has
74    /// already been triggered.
75    #[must_use = "Caller must check if the operation was successful"]
76    pub async fn try_shutdown(&self) -> bool {
77        self.try_trigger(true).await
78    }
79
80    /// Attempts to trigger a shutdown or restart.
81    ///
82    /// Returns true if successful, and false if a shutdown or restart has
83    /// already been triggered.
84    #[must_use = "Caller must check if the operation was successful"]
85    pub async fn try_trigger(&self, is_shutdown: bool) -> bool {
86        match self
87            .triggered
88            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Relaxed)
89        {
90            Ok(_) => {
91                self.is_shutdown.store(is_shutdown, Ordering::Relaxed);
92                self.notify.notify_waiters();
93                true
94            }
95            Err(_) => false,
96        }
97    }
98
99    /// Clears the triggered state.
100    pub fn clear(&self) {
101        self.is_shutdown.store(false, Ordering::Relaxed);
102        self.triggered.store(false, Ordering::Release);
103    }
104
105    /// Listener that resolves when a restart or shutdown is triggered. Returns
106    /// true if a shutdown was triggered, and false if a restart was triggered.
107    ///
108    /// If the controller is already in a triggered state, this will resolve
109    /// **after the next trigger**.
110    #[must_use = "Caller must await the listener to receive the signal"]
111    pub fn listener<'a>(&'a self) -> SoftCycleListener<'a> {
112        SoftCycleListener {
113            notify: self.notify.clone().notified_owned(),
114            controller: self,
115        }
116    }
117}
118
119#[cfg(feature = "global_instance")]
120#[cfg_attr(docsrs, doc(cfg(feature = "global_instance")))]
121mod global;
122
123#[cfg(feature = "global_instance")]
124#[cfg_attr(docsrs, doc(cfg(feature = "global_instance")))]
125pub use global::*;