Skip to main content

trueno/brick/
shutdown.rs

1//! Graceful Shutdown Coordination
2//!
3//! AWP-07: Two-phase shutdown with active operation tracking.
4
5use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
6use std::time::{Duration, Instant};
7
8// ----------------------------------------------------------------------------
9// AWP-07: Graceful Shutdown
10// ----------------------------------------------------------------------------
11
12/// Result of a graceful shutdown operation.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum ShutdownResult {
15    /// All operations completed cleanly.
16    Clean,
17    /// Timeout reached with operations still active.
18    Timeout {
19        /// Number of operations still active.
20        remaining: usize,
21    },
22}
23
24/// Graceful shutdown coordinator.
25///
26/// # Example
27/// ```rust
28/// use trueno::brick::GracefulShutdown;
29/// use std::time::Duration;
30///
31/// let shutdown = GracefulShutdown::new(Duration::from_secs(5));
32///
33/// // Register an operation
34/// let guard = shutdown.register().ok_or("shutdown in progress")?;
35///
36/// // ... do work ...
37///
38/// drop(guard);  // Operation complete
39///
40/// // Initiate shutdown
41/// let result = shutdown.shutdown();
42/// assert_eq!(result, trueno::brick::ShutdownResult::Clean);
43/// # Ok::<(), &'static str>(())
44/// ```
45pub struct GracefulShutdown {
46    /// Flag indicating shutdown has been requested.
47    shutdown_requested: AtomicBool,
48    /// Number of active operations.
49    active_count: AtomicUsize,
50    /// Shutdown timeout.
51    timeout: Duration,
52}
53
54impl GracefulShutdown {
55    /// Create a new shutdown coordinator.
56    pub fn new(timeout: Duration) -> Self {
57        Self {
58            shutdown_requested: AtomicBool::new(false),
59            active_count: AtomicUsize::new(0),
60            timeout,
61        }
62    }
63
64    /// Check if shutdown has been requested.
65    #[must_use]
66    pub fn is_shutdown_requested(&self) -> bool {
67        self.shutdown_requested.load(Ordering::Acquire)
68    }
69
70    /// Get the current active operation count.
71    #[must_use]
72    pub fn active_count(&self) -> usize {
73        self.active_count.load(Ordering::Acquire)
74    }
75
76    /// Register an active operation.
77    ///
78    /// Returns `None` if shutdown has already been requested.
79    pub fn register(&self) -> Option<ShutdownGuard<'_>> {
80        if self.is_shutdown_requested() {
81            return None; // Reject new operations during shutdown
82        }
83        self.active_count.fetch_add(1, Ordering::AcqRel);
84        Some(ShutdownGuard { shutdown: self })
85    }
86
87    /// Initiate graceful shutdown.
88    ///
89    /// This will:
90    /// 1. Stop accepting new operations
91    /// 2. Wait for in-flight operations to complete (up to timeout)
92    /// 3. Return the result
93    pub fn shutdown(&self) -> ShutdownResult {
94        // Phase 1: Stop accepting new operations
95        self.shutdown_requested.store(true, Ordering::Release);
96
97        // Phase 2: Wait for in-flight operations
98        let deadline = Instant::now() + self.timeout;
99
100        loop {
101            let active = self.active_count.load(Ordering::Acquire);
102            if active == 0 {
103                return ShutdownResult::Clean;
104            }
105            if Instant::now() >= deadline {
106                return ShutdownResult::Timeout { remaining: active };
107            }
108            std::thread::sleep(Duration::from_millis(10));
109        }
110    }
111
112    /// Reset the shutdown coordinator for reuse.
113    pub fn reset(&self) {
114        self.shutdown_requested.store(false, Ordering::Release);
115        // Note: active_count should already be 0 if shutdown completed cleanly
116    }
117}
118
119impl Default for GracefulShutdown {
120    fn default() -> Self {
121        Self::new(Duration::from_secs(30))
122    }
123}
124
125/// Guard that decrements active count on drop.
126pub struct ShutdownGuard<'a> {
127    shutdown: &'a GracefulShutdown,
128}
129
130impl Drop for ShutdownGuard<'_> {
131    fn drop(&mut self) {
132        self.shutdown.active_count.fetch_sub(1, Ordering::AcqRel);
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_shutdown_result_eq() {
142        assert_eq!(ShutdownResult::Clean, ShutdownResult::Clean);
143        assert_eq!(
144            ShutdownResult::Timeout { remaining: 5 },
145            ShutdownResult::Timeout { remaining: 5 }
146        );
147        assert_ne!(ShutdownResult::Clean, ShutdownResult::Timeout { remaining: 0 });
148    }
149
150    #[test]
151    fn test_graceful_shutdown_new() {
152        let shutdown = GracefulShutdown::new(Duration::from_secs(5));
153        assert!(!shutdown.is_shutdown_requested());
154        assert_eq!(shutdown.active_count(), 0);
155    }
156
157    #[test]
158    fn test_graceful_shutdown_default() {
159        let shutdown = GracefulShutdown::default();
160        assert!(!shutdown.is_shutdown_requested());
161        assert_eq!(shutdown.timeout, Duration::from_secs(30));
162    }
163
164    #[test]
165    fn test_graceful_shutdown_register() {
166        let shutdown = GracefulShutdown::new(Duration::from_secs(5));
167
168        let guard1 = shutdown.register();
169        assert!(guard1.is_some());
170        assert_eq!(shutdown.active_count(), 1);
171
172        let guard2 = shutdown.register();
173        assert!(guard2.is_some());
174        assert_eq!(shutdown.active_count(), 2);
175
176        drop(guard1);
177        assert_eq!(shutdown.active_count(), 1);
178
179        drop(guard2);
180        assert_eq!(shutdown.active_count(), 0);
181    }
182
183    #[test]
184    fn test_graceful_shutdown_clean() {
185        let shutdown = GracefulShutdown::new(Duration::from_millis(100));
186
187        // Register and immediately drop
188        let guard = shutdown.register();
189        drop(guard);
190
191        let result = shutdown.shutdown();
192        assert_eq!(result, ShutdownResult::Clean);
193    }
194
195    #[test]
196    fn test_graceful_shutdown_rejects_after_shutdown() {
197        let shutdown = GracefulShutdown::new(Duration::from_millis(10));
198
199        // Initiate shutdown
200        let result = shutdown.shutdown();
201        assert_eq!(result, ShutdownResult::Clean);
202
203        // Try to register after shutdown
204        let guard = shutdown.register();
205        assert!(guard.is_none(), "Should reject new operations after shutdown");
206    }
207
208    #[test]
209    fn test_graceful_shutdown_reset() {
210        let shutdown = GracefulShutdown::new(Duration::from_millis(10));
211
212        // Shutdown
213        shutdown.shutdown();
214        assert!(shutdown.is_shutdown_requested());
215
216        // Reset
217        shutdown.reset();
218        assert!(!shutdown.is_shutdown_requested());
219
220        // Should accept new operations again
221        let guard = shutdown.register();
222        assert!(guard.is_some());
223    }
224
225    /// FALSIFICATION TEST: Verify timeout behavior
226    ///
227    /// The shutdown coordinator MUST return Timeout when operations
228    /// don't complete within the specified duration.
229    #[test]
230    fn test_falsify_shutdown_timeout() {
231        let shutdown = GracefulShutdown::new(Duration::from_millis(50));
232
233        // Register an operation that we WON'T drop
234        let _guard = shutdown.register();
235        assert_eq!(shutdown.active_count(), 1);
236
237        // Initiate shutdown - should timeout since operation is still active
238        let start = Instant::now();
239        let result = shutdown.shutdown();
240        let elapsed = start.elapsed();
241
242        // CRITICAL ASSERTIONS
243        match result {
244            ShutdownResult::Timeout { remaining } => {
245                assert_eq!(remaining, 1, "Should report exactly 1 remaining operation");
246            }
247            ShutdownResult::Clean => {
248                panic!("FALSIFICATION FAILED: Shutdown reported Clean with active operation");
249            }
250        }
251
252        // Verify we actually waited (within tolerance)
253        assert!(
254            elapsed >= Duration::from_millis(40),
255            "FALSIFICATION FAILED: Shutdown returned too early ({:?} < 40ms)",
256            elapsed
257        );
258    }
259
260    /// FALSIFICATION TEST: Verify guard drop decrements count
261    ///
262    /// If the guard drop doesn't work correctly, the system will
263    /// either hang forever or report wrong active counts.
264    #[test]
265    fn test_falsify_guard_drop_semantics() {
266        let shutdown = GracefulShutdown::new(Duration::from_secs(1));
267
268        // Create many guards
269        let mut guards: Vec<_> = (0..100).filter_map(|_| shutdown.register()).collect();
270        assert_eq!(shutdown.active_count(), 100, "FALSIFICATION FAILED: Not all guards registered");
271
272        // Drop half by truncating the vector (keeps first 50, drops last 50)
273        guards.truncate(50);
274
275        // CRITICAL: Active count must be exactly 50
276        // If fetch_sub has a bug (e.g., fetch_add by mistake), this catches it
277        assert_eq!(
278            shutdown.active_count(),
279            50,
280            "FALSIFICATION FAILED: Guard drop did not correctly decrement count"
281        );
282
283        // Drop remaining 50
284        drop(guards);
285        assert_eq!(
286            shutdown.active_count(),
287            0,
288            "FALSIFICATION FAILED: Final drop did not clear all guards"
289        );
290    }
291
292    /// FALSIFICATION TEST: Verify concurrent registration rejection
293    ///
294    /// Once shutdown starts, NO new operations should be accepted,
295    /// even if they race with the shutdown call.
296    #[test]
297    fn test_falsify_rejection_after_shutdown_flag() {
298        let shutdown = GracefulShutdown::new(Duration::from_secs(5));
299
300        // Manually set shutdown flag (simulating shutdown start)
301        shutdown.shutdown_requested.store(true, Ordering::Release);
302
303        // Try to register many operations
304        let mut accepted = 0;
305        for _ in 0..100 {
306            if shutdown.register().is_some() {
307                accepted += 1;
308            }
309        }
310
311        // CRITICAL: NO operations should have been accepted
312        assert_eq!(
313            accepted, 0,
314            "FALSIFICATION FAILED: {} operations accepted after shutdown flag set",
315            accepted
316        );
317    }
318}