aurum_core/runtime/
lifecycle.rs1use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
4use std::time::{Duration, Instant};
5
6const STATE_RUNNING: u8 = 0;
7const STATE_SHUTTING_DOWN: u8 = 1;
8const STATE_STOPPED: u8 = 2;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum LifecycleState {
13 Running,
14 ShuttingDown,
15 Stopped,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum ShutdownError {
21 Busy { active: usize },
23}
24
25#[derive(Debug)]
30pub struct Lifecycle {
31 state: AtomicU8,
32 active: AtomicUsize,
33}
34
35impl Default for Lifecycle {
36 fn default() -> Self {
37 Self::new()
38 }
39}
40
41impl Lifecycle {
42 pub fn new() -> Self {
43 Self {
44 state: AtomicU8::new(STATE_RUNNING),
45 active: AtomicUsize::new(0),
46 }
47 }
48
49 pub fn state(&self) -> LifecycleState {
50 match self.state.load(Ordering::SeqCst) {
51 STATE_RUNNING => LifecycleState::Running,
52 STATE_SHUTTING_DOWN => LifecycleState::ShuttingDown,
53 _ => LifecycleState::Stopped,
54 }
55 }
56
57 pub fn active_ops(&self) -> usize {
58 self.active.load(Ordering::SeqCst)
59 }
60
61 pub fn try_begin_op(&self) -> Result<OpAdmission<'_>, AdmissionError> {
66 let s = self.state.load(Ordering::SeqCst);
68 if s != STATE_RUNNING {
69 return Err(AdmissionError::NotRunning {
70 state: decode_state(s),
71 });
72 }
73 self.active.fetch_add(1, Ordering::SeqCst);
75 let s2 = self.state.load(Ordering::SeqCst);
76 if s2 != STATE_RUNNING {
77 self.active.fetch_sub(1, Ordering::SeqCst);
78 return Err(AdmissionError::NotRunning {
79 state: decode_state(s2),
80 });
81 }
82 Ok(OpAdmission { life: self })
83 }
84
85 pub(crate) fn end_op(&self) {
86 self.active.fetch_sub(1, Ordering::SeqCst);
87 }
88
89 pub fn shutdown(&self, timeout: Duration) -> Result<(), ShutdownError> {
98 let prev = self.state.load(Ordering::SeqCst);
99 if prev == STATE_STOPPED {
100 return Ok(());
101 }
102 self.state
104 .compare_exchange(
105 STATE_RUNNING,
106 STATE_SHUTTING_DOWN,
107 Ordering::SeqCst,
108 Ordering::SeqCst,
109 )
110 .ok();
111 if self.state.load(Ordering::SeqCst) == STATE_STOPPED {
113 return Ok(());
114 }
115 self.state.store(STATE_SHUTTING_DOWN, Ordering::SeqCst);
116
117 let deadline = Instant::now() + timeout;
118 while self.active.load(Ordering::SeqCst) > 0 {
119 if Instant::now() >= deadline {
120 let active = self.active.load(Ordering::SeqCst);
121 return Err(ShutdownError::Busy { active });
122 }
123 std::thread::sleep(Duration::from_millis(5));
124 }
125 self.state.store(STATE_STOPPED, Ordering::SeqCst);
126 Ok(())
127 }
128
129 pub fn mark_stopped_if_idle(&self) -> bool {
131 if self.active.load(Ordering::SeqCst) == 0 {
132 self.state.store(STATE_STOPPED, Ordering::SeqCst);
133 true
134 } else {
135 false
136 }
137 }
138
139 pub fn is_running(&self) -> bool {
141 self.state.load(Ordering::SeqCst) == STATE_RUNNING
142 }
143}
144
145fn decode_state(s: u8) -> LifecycleState {
146 match s {
147 STATE_RUNNING => LifecycleState::Running,
148 STATE_SHUTTING_DOWN => LifecycleState::ShuttingDown,
149 _ => LifecycleState::Stopped,
150 }
151}
152
153#[derive(Debug, Clone, PartialEq, Eq)]
155pub enum AdmissionError {
156 NotRunning { state: LifecycleState },
157}
158
159impl std::fmt::Display for AdmissionError {
160 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161 match self {
162 Self::NotRunning { state } => {
163 write!(f, "runtime is not accepting work (state={state:?})")
164 }
165 }
166 }
167}
168
169impl std::error::Error for AdmissionError {}
170
171pub struct OpAdmission<'a> {
173 life: &'a Lifecycle,
174}
175
176impl Drop for OpAdmission<'_> {
177 fn drop(&mut self) {
178 self.life.end_op();
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use std::sync::Arc;
186 use std::thread;
187
188 #[test]
189 fn admit_and_release() {
190 let life = Lifecycle::new();
191 let g = life.try_begin_op().unwrap();
192 assert_eq!(life.active_ops(), 1);
193 drop(g);
194 assert_eq!(life.active_ops(), 0);
195 }
196
197 #[test]
198 fn shutdown_blocks_admission() {
199 let life = Lifecycle::new();
200 life.shutdown(Duration::from_millis(100)).unwrap();
201 assert!(matches!(
202 life.try_begin_op(),
203 Err(AdmissionError::NotRunning { .. })
204 ));
205 assert_eq!(life.state(), LifecycleState::Stopped);
206 }
207
208 #[test]
209 fn shutdown_waits_for_active() {
210 let life = Arc::new(Lifecycle::new());
211 let g = life.try_begin_op().unwrap();
212 let life2 = Arc::clone(&life);
213 let h = thread::spawn(move || life2.shutdown(Duration::from_secs(2)));
214 thread::sleep(Duration::from_millis(50));
215 assert_eq!(life.state(), LifecycleState::ShuttingDown);
216 drop(g);
217 h.join().unwrap().unwrap();
218 assert_eq!(life.state(), LifecycleState::Stopped);
219 }
220
221 #[test]
222 fn shutdown_timeout_busy() {
223 let life = Lifecycle::new();
224 let _g = life.try_begin_op().unwrap();
225 let err = life.shutdown(Duration::from_millis(30)).unwrap_err();
226 assert!(matches!(err, ShutdownError::Busy { active: 1 }));
227 assert_eq!(life.state(), LifecycleState::ShuttingDown);
228 }
229
230 #[test]
231 fn shutdown_idempotent_when_stopped() {
232 let life = Lifecycle::new();
233 life.shutdown(Duration::from_millis(100)).unwrap();
234 life.shutdown(Duration::from_millis(100)).unwrap();
235 assert_eq!(life.state(), LifecycleState::Stopped);
236 }
237
238 #[test]
239 fn race_admission_with_shutdown() {
240 let life = Arc::new(Lifecycle::new());
241 let mut handles = vec![];
242 for _ in 0..8 {
243 let l = Arc::clone(&life);
244 handles.push(thread::spawn(move || {
245 for _ in 0..100 {
246 if let Ok(g) = l.try_begin_op() {
247 thread::sleep(Duration::from_micros(50));
248 drop(g);
249 }
250 }
251 }));
252 }
253 thread::sleep(Duration::from_millis(5));
254 let _ = life.shutdown(Duration::from_secs(2));
255 for h in handles {
256 h.join().unwrap();
257 }
258 assert_eq!(life.active_ops(), 0);
259 }
260}