Skip to main content

mabi_core/
lifecycle.rs

1//! Device lifecycle management traits and types.
2//!
3//! This module provides a clean separation of lifecycle concerns from
4//! device operations, improving testability and maintainability.
5//!
6//! # Lifecycle States
7//!
8//! ```text
9//! ┌──────────────┐
10//! │ Uninitialized│
11//! └──────┬───────┘
12//!        │ initialize()
13//!        ▼
14//! ┌──────────────┐
15//! │ Initializing │──── Error ────┐
16//! └──────┬───────┘               │
17//!        │                       ▼
18//!        ▼               ┌───────────┐
19//! ┌──────────────┐       │   Error   │
20//! │   Offline    │◄──────┤           │
21//! └──────┬───────┘       └───────────┘
22//!        │ start()              ▲
23//!        ▼                      │
24//! ┌──────────────┐              │
25//! │    Online    │─── error ────┘
26//! └──────┬───────┘
27//!        │ stop()
28//!        ▼
29//! ┌──────────────┐
30//! │ ShuttingDown │
31//! └──────┬───────┘
32//!        │
33//!        ▼
34//! ┌──────────────┐
35//! │   Offline    │
36//! └──────────────┘
37//! ```
38
39use std::time::{Duration, Instant};
40
41use async_trait::async_trait;
42use serde::{Deserialize, Serialize};
43
44use crate::device::DeviceState;
45use crate::error::Result;
46
47/// Lifecycle management trait for devices.
48///
49/// This trait separates lifecycle concerns from regular device operations,
50/// making it easier to implement custom lifecycle behavior and testing.
51#[async_trait]
52pub trait DeviceLifecycle: Send + Sync {
53    /// Get the current lifecycle state.
54    fn state(&self) -> DeviceState;
55
56    /// Check if the device is in an operational state.
57    fn is_operational(&self) -> bool {
58        self.state().is_operational()
59    }
60
61    /// Check if the device can accept requests.
62    fn can_accept_requests(&self) -> bool {
63        self.state().can_accept_requests()
64    }
65
66    /// Initialize the device.
67    ///
68    /// This should set up any necessary resources and move the device
69    /// from `Uninitialized` to `Offline` state.
70    async fn initialize(&mut self) -> Result<()>;
71
72    /// Start the device.
73    ///
74    /// This should make the device operational and move it to `Online` state.
75    async fn start(&mut self) -> Result<()>;
76
77    /// Stop the device.
78    ///
79    /// This should gracefully stop the device and move it to `Offline` state.
80    async fn stop(&mut self) -> Result<()>;
81
82    /// Handle errors during operation.
83    ///
84    /// This is called when an error occurs during device operation.
85    /// The default implementation transitions to `Error` state.
86    async fn on_error(&mut self, _error: &crate::error::Error) -> Result<()> {
87        Ok(())
88    }
89
90    /// Attempt to recover from an error state.
91    ///
92    /// Returns true if recovery was successful.
93    async fn recover(&mut self) -> Result<bool> {
94        Ok(false)
95    }
96}
97
98/// Lifecycle event types.
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub enum LifecycleEvent {
101    /// Device initialized.
102    Initialized {
103        device_id: String,
104        timestamp: chrono::DateTime<chrono::Utc>,
105    },
106    /// Device started.
107    Started {
108        device_id: String,
109        timestamp: chrono::DateTime<chrono::Utc>,
110    },
111    /// Device stopped.
112    Stopped {
113        device_id: String,
114        timestamp: chrono::DateTime<chrono::Utc>,
115        reason: StopReason,
116    },
117    /// State changed.
118    StateChanged {
119        device_id: String,
120        old_state: DeviceState,
121        new_state: DeviceState,
122        timestamp: chrono::DateTime<chrono::Utc>,
123    },
124    /// Error occurred.
125    Error {
126        device_id: String,
127        error: String,
128        timestamp: chrono::DateTime<chrono::Utc>,
129    },
130    /// Recovery attempted.
131    RecoveryAttempted {
132        device_id: String,
133        success: bool,
134        timestamp: chrono::DateTime<chrono::Utc>,
135    },
136}
137
138/// Reason for device stop.
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
140pub enum StopReason {
141    /// Normal shutdown requested by user.
142    UserRequested,
143    /// Shutdown due to error.
144    Error,
145    /// Shutdown for maintenance.
146    Maintenance,
147    /// System shutdown.
148    SystemShutdown,
149    /// Timeout.
150    Timeout,
151}
152
153/// Lifecycle state machine that manages state transitions.
154#[derive(Debug, Clone)]
155pub struct LifecycleStateMachine {
156    current_state: DeviceState,
157    last_transition: Option<Instant>,
158    error_count: u32,
159    max_retries: u32,
160    retry_delay: Duration,
161}
162
163impl LifecycleStateMachine {
164    /// Create a new state machine.
165    pub fn new() -> Self {
166        Self {
167            current_state: DeviceState::Uninitialized,
168            last_transition: None,
169            error_count: 0,
170            max_retries: 3,
171            retry_delay: Duration::from_secs(1),
172        }
173    }
174
175    /// Create with custom retry settings.
176    pub fn with_retries(mut self, max_retries: u32, retry_delay: Duration) -> Self {
177        self.max_retries = max_retries;
178        self.retry_delay = retry_delay;
179        self
180    }
181
182    /// Get the current state.
183    pub fn state(&self) -> DeviceState {
184        self.current_state
185    }
186
187    /// Check if a transition is valid.
188    pub fn can_transition_to(&self, target: DeviceState) -> bool {
189        match (self.current_state, target) {
190            // From Uninitialized
191            (DeviceState::Uninitialized, DeviceState::Initializing) => true,
192            (DeviceState::Uninitialized, DeviceState::Error) => true,
193
194            // From Initializing
195            (DeviceState::Initializing, DeviceState::Offline) => true,
196            (DeviceState::Initializing, DeviceState::Online) => true,
197            (DeviceState::Initializing, DeviceState::Error) => true,
198
199            // From Offline
200            (DeviceState::Offline, DeviceState::Online) => true,
201            (DeviceState::Offline, DeviceState::Error) => true,
202
203            // From Online
204            (DeviceState::Online, DeviceState::Offline) => true,
205            (DeviceState::Online, DeviceState::ShuttingDown) => true,
206            (DeviceState::Online, DeviceState::Error) => true,
207
208            // From ShuttingDown
209            (DeviceState::ShuttingDown, DeviceState::Offline) => true,
210            (DeviceState::ShuttingDown, DeviceState::Error) => true,
211
212            // From Error
213            (DeviceState::Error, DeviceState::Offline) => true,
214            (DeviceState::Error, DeviceState::Initializing) => true,
215            (DeviceState::Error, DeviceState::Uninitialized) => true,
216
217            _ => false,
218        }
219    }
220
221    /// Attempt to transition to a new state.
222    pub fn transition_to(&mut self, target: DeviceState) -> Result<DeviceState> {
223        if !self.can_transition_to(target) {
224            return Err(crate::error::Error::Engine(format!(
225                "Invalid state transition: {:?} -> {:?}",
226                self.current_state, target
227            )));
228        }
229
230        let old_state = self.current_state;
231        self.current_state = target;
232        self.last_transition = Some(Instant::now());
233
234        // Reset error count on successful non-error transition
235        if target != DeviceState::Error {
236            self.error_count = 0;
237        }
238
239        Ok(old_state)
240    }
241
242    /// Record an error and check if retries are exhausted.
243    pub fn record_error(&mut self) -> bool {
244        self.error_count += 1;
245        self.error_count > self.max_retries
246    }
247
248    /// Get the current error count.
249    pub fn error_count(&self) -> u32 {
250        self.error_count
251    }
252
253    /// Reset the error count.
254    pub fn reset_errors(&mut self) {
255        self.error_count = 0;
256    }
257
258    /// Get time since last transition.
259    pub fn time_in_state(&self) -> Option<Duration> {
260        self.last_transition.map(|t| t.elapsed())
261    }
262
263    /// Check if retry delay has passed.
264    pub fn can_retry(&self) -> bool {
265        self.last_transition
266            .map(|t| t.elapsed() >= self.retry_delay)
267            .unwrap_or(true)
268    }
269}
270
271impl Default for LifecycleStateMachine {
272    fn default() -> Self {
273        Self::new()
274    }
275}
276
277/// Lifecycle hook trait for extending lifecycle behavior.
278///
279/// Implement this trait to add custom behavior at lifecycle transitions.
280#[async_trait]
281pub trait LifecycleHook: Send + Sync {
282    /// Called before initialization.
283    async fn before_init(&self) -> Result<()> {
284        Ok(())
285    }
286
287    /// Called after successful initialization.
288    async fn after_init(&self) -> Result<()> {
289        Ok(())
290    }
291
292    /// Called before starting.
293    async fn before_start(&self) -> Result<()> {
294        Ok(())
295    }
296
297    /// Called after successful start.
298    async fn after_start(&self) -> Result<()> {
299        Ok(())
300    }
301
302    /// Called before stopping.
303    async fn before_stop(&self) -> Result<()> {
304        Ok(())
305    }
306
307    /// Called after successful stop.
308    async fn after_stop(&self) -> Result<()> {
309        Ok(())
310    }
311
312    /// Called when entering error state.
313    async fn on_error(&self, _error: &crate::error::Error) -> Result<()> {
314        Ok(())
315    }
316}
317
318/// A no-op lifecycle hook for use as a default.
319pub struct NoOpLifecycleHook;
320
321#[async_trait]
322impl LifecycleHook for NoOpLifecycleHook {}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    #[test]
329    fn test_lifecycle_state_machine_transitions() {
330        let mut sm = LifecycleStateMachine::new();
331
332        assert_eq!(sm.state(), DeviceState::Uninitialized);
333
334        // Valid transition
335        sm.transition_to(DeviceState::Initializing).unwrap();
336        assert_eq!(sm.state(), DeviceState::Initializing);
337
338        sm.transition_to(DeviceState::Offline).unwrap();
339        assert_eq!(sm.state(), DeviceState::Offline);
340
341        sm.transition_to(DeviceState::Online).unwrap();
342        assert_eq!(sm.state(), DeviceState::Online);
343    }
344
345    #[test]
346    fn test_lifecycle_state_machine_invalid_transition() {
347        let mut sm = LifecycleStateMachine::new();
348
349        // Invalid transition: Uninitialized -> Online
350        let result = sm.transition_to(DeviceState::Online);
351        assert!(result.is_err());
352    }
353
354    #[test]
355    fn test_lifecycle_state_machine_error_handling() {
356        let mut sm = LifecycleStateMachine::new().with_retries(3, Duration::from_millis(100));
357
358        // Record errors
359        assert!(!sm.record_error()); // 1st error
360        assert!(!sm.record_error()); // 2nd error
361        assert!(!sm.record_error()); // 3rd error
362        assert!(sm.record_error()); // 4th error - exhausted
363
364        assert_eq!(sm.error_count(), 4);
365
366        sm.reset_errors();
367        assert_eq!(sm.error_count(), 0);
368    }
369
370    #[test]
371    fn test_lifecycle_valid_transitions() {
372        let sm = LifecycleStateMachine::new();
373
374        assert!(sm.can_transition_to(DeviceState::Initializing));
375        assert!(sm.can_transition_to(DeviceState::Error));
376        assert!(!sm.can_transition_to(DeviceState::Online));
377        assert!(!sm.can_transition_to(DeviceState::ShuttingDown));
378    }
379}