Skip to main content

kunobi_daemon/
lifecycle.rs

1use std::sync::{Arc, Mutex};
2use tokio::sync::watch;
3use tokio::time::Instant;
4
5#[derive(Clone, Copy, Default)]
6struct State {
7    draining: bool,
8    active: usize,
9    drain_started: Option<Instant>,
10}
11
12/// A one-way request admission gate shared by all sessions of a daemon.
13///
14/// Call [`begin`](Self::begin) for each operation, including operations arriving
15/// on existing connections. Keep the returned guard until its reply has been
16/// flushed. Closing a listener alone does not close persistent sessions.
17pub struct Lifecycle {
18    state: Mutex<State>,
19    changes: watch::Sender<()>,
20    observations: Option<Arc<crate::observation::Observations>>,
21}
22
23impl Default for Lifecycle {
24    fn default() -> Self {
25        Self {
26            state: Mutex::new(State::default()),
27            changes: watch::channel(()).0,
28            observations: None,
29        }
30    }
31}
32
33impl Lifecycle {
34    /// Attach local events; the consumer owns their export and retention.
35    pub fn observed(observations: Arc<crate::observation::Observations>) -> Self {
36        Self {
37            observations: Some(observations),
38            ..Self::default()
39        }
40    }
41
42    /// Observe admission without waiting for requests or transport writes.
43    pub fn snapshot(&self) -> LifecycleSnapshot {
44        let state = self.state.lock().unwrap_or_else(|e| e.into_inner());
45        LifecycleSnapshot {
46            active: state.active,
47            draining: state.draining,
48            draining_for: state.drain_started.map(|started| started.elapsed()),
49        }
50    }
51
52    fn record(&self, event: crate::observation::Event) {
53        if let Some(observations) = &self.observations {
54            observations.record(event);
55        }
56    }
57    /// Admit an operation unless draining has started.
58    pub fn begin(self: &Arc<Self>) -> Option<RequestGuard> {
59        let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
60        if state.draining {
61            return None;
62        }
63        state.active += 1;
64        Some(RequestGuard(Arc::clone(self)))
65    }
66
67    /// Close admission and wake shutdown observers. Only the first call is true.
68    pub fn start_drain(&self) -> bool {
69        let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
70        if state.draining {
71            return false;
72        }
73        state.draining = true;
74        state.drain_started = Some(Instant::now());
75        self.record(crate::observation::Event::DrainStarted);
76        if state.active == 0 {
77            self.record(crate::observation::Event::DrainCompleted);
78        }
79        self.changes.send_replace(());
80        true
81    }
82
83    /// Whether a new operation can still acquire a guard.
84    pub fn accepting_calls(&self) -> bool {
85        !self
86            .state
87            .lock()
88            .unwrap_or_else(|e| e.into_inner())
89            .draining
90    }
91
92    /// Wait until admission has closed, including when it closed before this call.
93    pub async fn draining(&self) {
94        let mut changes = self.changes.subscribe();
95        loop {
96            changes.borrow_and_update();
97            if self
98                .state
99                .lock()
100                .unwrap_or_else(|e| e.into_inner())
101                .draining
102            {
103                return;
104            }
105            changes.changed().await.expect("self keeps sender alive");
106        }
107    }
108
109    /// Close admission and wait for every guard without a timeout.
110    pub async fn drain(&self) {
111        self.start_drain();
112        let mut changes = self.changes.subscribe();
113        loop {
114            changes.borrow_and_update();
115            let active = self.state.lock().unwrap_or_else(|e| e.into_inner()).active;
116            if active == 0 {
117                return;
118            }
119            changes.changed().await.expect("self keeps sender alive");
120        }
121    }
122
123    /// Close admission and wait for all guards, under a single deadline.
124    ///
125    /// A timeout leaves admission closed and the operations running. The caller
126    /// decides whether they can be cancelled or must be allowed to finish.
127    pub async fn drain_until(&self, deadline: Instant) -> DrainOutcome {
128        if tokio::time::timeout_at(deadline, self.drain())
129            .await
130            .is_ok()
131        {
132            return DrainOutcome::Complete;
133        }
134        let active = self.state.lock().unwrap_or_else(|e| e.into_inner()).active;
135        if active == 0 {
136            DrainOutcome::Complete
137        } else {
138            self.record(crate::observation::Event::DrainTimedOut);
139            DrainOutcome::TimedOut { active }
140        }
141    }
142}
143
144/// Current request admission and drain state.
145#[derive(Clone, Copy, Debug, PartialEq, Eq)]
146pub struct LifecycleSnapshot {
147    /// Admitted requests whose guards have not been dropped.
148    pub active: usize,
149    /// Whether admission has closed.
150    pub draining: bool,
151    /// Elapsed time since admission closed, including time after completion.
152    pub draining_for: Option<std::time::Duration>,
153}
154
155/// Result of closing admission and waiting for admitted operations.
156#[derive(Debug, PartialEq, Eq)]
157pub enum DrainOutcome {
158    /// Every admitted operation released its guard.
159    Complete,
160    /// Work remains; the crate has not cancelled it.
161    TimedOut {
162        /// Number of guards still held at the deadline observation.
163        active: usize,
164    },
165}
166
167/// Keeps an admitted operation in the drain count until dropped.
168#[must_use = "keep the guard alive until the operation and its reply finish"]
169pub struct RequestGuard(Arc<Lifecycle>);
170
171impl Drop for RequestGuard {
172    fn drop(&mut self) {
173        let mut state = self.0.state.lock().unwrap_or_else(|e| e.into_inner());
174        state.active -= 1;
175        if state.draining && state.active == 0 {
176            self.0.record(crate::observation::Event::DrainCompleted);
177            self.0.changes.send_replace(());
178        }
179    }
180}