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    pub(crate) 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    /// Run `operation` unless admission closes first. `None` means draining
110    /// started, for example while a persistent connection waited for its next
111    /// request; the caller then stops reading from it.
112    pub async fn unless_draining<T>(
113        &self,
114        operation: impl std::future::Future<Output = T>,
115    ) -> Option<T> {
116        tokio::select! {
117            biased;
118            () = self.draining() => None,
119            value = operation => Some(value),
120        }
121    }
122
123    /// Close admission and wait for every guard without a timeout.
124    pub async fn drain(&self) {
125        self.start_drain();
126        let mut changes = self.changes.subscribe();
127        loop {
128            changes.borrow_and_update();
129            let active = self.state.lock().unwrap_or_else(|e| e.into_inner()).active;
130            if active == 0 {
131                return;
132            }
133            changes.changed().await.expect("self keeps sender alive");
134        }
135    }
136
137    /// Close admission and wait for all guards, under a single deadline.
138    ///
139    /// A timeout leaves admission closed and the operations running. The caller
140    /// decides whether they can be cancelled or must be allowed to finish.
141    pub async fn drain_until(&self, deadline: Instant) -> DrainOutcome {
142        if tokio::time::timeout_at(deadline, self.drain())
143            .await
144            .is_ok()
145        {
146            return DrainOutcome::Complete;
147        }
148        let active = self.state.lock().unwrap_or_else(|e| e.into_inner()).active;
149        if active == 0 {
150            DrainOutcome::Complete
151        } else {
152            self.record(crate::observation::Event::DrainTimedOut);
153            DrainOutcome::TimedOut { active }
154        }
155    }
156}
157
158/// Current request admission and drain state.
159#[derive(Clone, Copy, Debug, PartialEq, Eq)]
160#[non_exhaustive]
161pub struct LifecycleSnapshot {
162    /// Admitted requests whose guards have not been dropped.
163    pub active: usize,
164    /// Whether admission has closed.
165    pub draining: bool,
166    /// Elapsed time since admission closed, including time after completion.
167    pub draining_for: Option<std::time::Duration>,
168}
169
170/// Result of closing admission and waiting for admitted operations.
171#[derive(Debug, PartialEq, Eq)]
172#[non_exhaustive]
173pub enum DrainOutcome {
174    /// Every admitted operation released its guard.
175    Complete,
176    /// Work remains; the crate has not cancelled it.
177    TimedOut {
178        /// Number of guards still held at the deadline observation.
179        active: usize,
180    },
181}
182
183/// Keeps an admitted operation in the drain count until dropped.
184#[must_use = "keep the guard alive until the operation and its reply finish"]
185pub struct RequestGuard(Arc<Lifecycle>);
186
187impl Drop for RequestGuard {
188    fn drop(&mut self) {
189        let mut state = self.0.state.lock().unwrap_or_else(|e| e.into_inner());
190        state.active -= 1;
191        if state.draining && state.active == 0 {
192            self.0.record(crate::observation::Event::DrainCompleted);
193            self.0.changes.send_replace(());
194        }
195    }
196}