mecha10_core/
lifecycle.rs

1//! Lifecycle event schemas for mode-based node management
2//!
3//! This module defines the event types used for dynamic lifecycle management:
4//! - Mode change requests and confirmations
5//! - Node lifecycle events
6//! - Node status reporting
7//!
8//! # Event Flow
9//!
10//! ```text
11//! CLI → ModeRequest → Runtime Lifecycle → Supervisor → Nodes
12//!                                       ↓
13//!                                 ModeChanged
14//! ```
15//!
16//! # Example
17//!
18//! ```rust,ignore
19//! use mecha10_core::lifecycle::*;
20//!
21//! // Request mode change
22//! context.publish(
23//!     TOPIC_MODE_REQUEST,
24//!     &ModeRequest {
25//!         mode: "simulation".to_string(),
26//!         requested_by: "cli".to_string(),
27//!         timestamp: get_timestamp_ms(),
28//!     }
29//! ).await?;
30//!
31//! // Listen for mode changes
32//! let mut sub = context.subscribe(TOPIC_MODE_CHANGED).await?;
33//! while let Some(msg) = sub.next().await {
34//!     let event: ModeChanged = msg.decode()?;
35//!     println!("Mode changed to: {}", event.current_mode);
36//! }
37//! ```
38
39use serde::{Deserialize, Serialize};
40
41// ========== Mode Management Events ==========
42
43/// Request to change operational mode
44///
45/// Published by CLI or nodes to request a mode transition.
46/// Runtime lifecycle manager subscribes to these requests.
47#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
48pub struct ModeRequest {
49    /// Target mode name (e.g., "simulation", "teleop", "autonomous")
50    pub mode: String,
51
52    /// Who requested the change (e.g., "cli", "mission-controller")
53    pub requested_by: String,
54
55    /// Unix timestamp in milliseconds
56    pub timestamp: u64,
57}
58
59/// Notification that mode has changed successfully
60///
61/// Published by runtime lifecycle manager after mode transition completes.
62/// Nodes can subscribe to react to mode changes.
63#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
64pub struct ModeChanged {
65    /// Previous mode name
66    pub previous_mode: String,
67
68    /// New current mode name
69    pub current_mode: String,
70
71    /// Nodes that were started during transition
72    pub nodes_started: Vec<String>,
73
74    /// Nodes that were stopped during transition
75    pub nodes_stopped: Vec<String>,
76
77    /// Unix timestamp in milliseconds
78    pub timestamp: u64,
79}
80
81/// Error during mode transition
82///
83/// Published by runtime lifecycle manager if mode change fails.
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
85pub struct ModeError {
86    /// Requested mode that failed
87    pub mode: String,
88
89    /// Error message
90    pub error: String,
91
92    /// Current mode (unchanged)
93    pub current_mode: String,
94
95    /// Unix timestamp in milliseconds
96    pub timestamp: u64,
97}
98
99// ========== Node Lifecycle Events ==========
100
101/// Lifecycle action for a specific node (internal to runtime)
102///
103/// Note: These events are primarily for internal runtime use.
104/// Most users should use mode requests instead.
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
106pub struct NodeLifecycleEvent {
107    /// Node name
108    pub node: String,
109
110    /// Action to perform
111    pub action: NodeLifecycleAction,
112
113    /// Reason for action
114    pub reason: String,
115
116    /// Target mode context (if mode change)
117    #[serde(default, skip_serializing_if = "Option::is_none")]
118    pub target_mode: Option<String>,
119
120    /// Graceful shutdown timeout in milliseconds
121    #[serde(default = "default_graceful_timeout")]
122    pub graceful_timeout_ms: u64,
123
124    /// Node-specific configuration
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    pub config: Option<serde_json::Value>,
127}
128
129/// Actions that can be performed on a node
130#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
131#[serde(rename_all = "lowercase")]
132pub enum NodeLifecycleAction {
133    /// Start the node
134    Start,
135
136    /// Stop the node gracefully
137    Stop,
138
139    /// Force kill the node (if graceful stop times out)
140    Kill,
141
142    /// Restart the node (stop then start)
143    Restart,
144}
145
146/// Node status report
147///
148/// Published by runtime supervisor to report node state changes.
149#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
150pub struct NodeStatusEvent {
151    /// Node name
152    pub node: String,
153
154    /// Current status
155    pub status: NodeLifecycleStatus,
156
157    /// Process ID (if running)
158    #[serde(default, skip_serializing_if = "Option::is_none")]
159    pub pid: Option<u32>,
160
161    /// Error message (if failed)
162    #[serde(default, skip_serializing_if = "Option::is_none")]
163    pub error: Option<String>,
164
165    /// Unix timestamp in milliseconds
166    pub timestamp: u64,
167}
168
169/// Lifecycle status of a node
170///
171/// This represents the operational state of a node process.
172/// Note: This is different from `discovery::NodeStatus` which is for service discovery.
173#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
174#[serde(rename_all = "lowercase")]
175pub enum NodeLifecycleStatus {
176    /// Node is starting up
177    Starting,
178
179    /// Node successfully started
180    Started,
181
182    /// Node is running normally
183    Running,
184
185    /// Node is shutting down
186    Stopping,
187
188    /// Node has stopped
189    Stopped,
190
191    /// Node failed to start or crashed
192    Failed,
193}
194
195// ========== Topic Constants ==========
196
197/// Topic for mode change requests
198///
199/// Format: `system/lifecycle/mode/request`
200///
201/// Payload: [`ModeRequest`]
202pub const TOPIC_MODE_REQUEST: &str = "system/lifecycle/mode/request";
203
204/// Topic for mode change confirmations
205///
206/// Format: `system/lifecycle/mode/changed`
207///
208/// Payload: [`ModeChanged`]
209pub const TOPIC_MODE_CHANGED: &str = "system/lifecycle/mode/changed";
210
211/// Topic for mode change errors
212///
213/// Format: `system/lifecycle/mode/error`
214///
215/// Payload: [`ModeError`]
216pub const TOPIC_MODE_ERROR: &str = "system/lifecycle/mode/error";
217
218/// Get topic for node lifecycle events (internal)
219///
220/// Format: `system/node/{node}/lifecycle`
221///
222/// Payload: [`NodeLifecycleEvent`]
223///
224/// Note: Primarily for internal runtime use
225pub fn topic_node_lifecycle(node: &str) -> String {
226    format!("system/node/{}/lifecycle", node)
227}
228
229/// Get topic for node status events
230///
231/// Format: `system/node/{node}/status`
232///
233/// Payload: [`NodeStatusEvent`]
234pub fn topic_node_status(node: &str) -> String {
235    format!("system/node/{}/status", node)
236}
237
238// ========== Helper Functions ==========
239
240/// Default graceful shutdown timeout (5 seconds)
241fn default_graceful_timeout() -> u64 {
242    5000
243}
244
245/// Get current timestamp in milliseconds since Unix epoch
246pub fn get_timestamp_ms() -> u64 {
247    std::time::SystemTime::now()
248        .duration_since(std::time::UNIX_EPOCH)
249        .unwrap_or_default()
250        .as_millis() as u64
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[test]
258    fn test_mode_request_serialization() {
259        let request = ModeRequest {
260            mode: "simulation".to_string(),
261            requested_by: "cli".to_string(),
262            timestamp: 1234567890,
263        };
264
265        let json = serde_json::to_string(&request).unwrap();
266        let deserialized: ModeRequest = serde_json::from_str(&json).unwrap();
267
268        assert_eq!(request, deserialized);
269    }
270
271    #[test]
272    fn test_mode_changed_serialization() {
273        let changed = ModeChanged {
274            previous_mode: "startup".to_string(),
275            current_mode: "simulation".to_string(),
276            nodes_started: vec!["sim-bridge".to_string(), "object-detector".to_string()],
277            nodes_stopped: vec!["imu".to_string(), "motor".to_string()],
278            timestamp: 1234567890,
279        };
280
281        let json = serde_json::to_string(&changed).unwrap();
282        let deserialized: ModeChanged = serde_json::from_str(&json).unwrap();
283
284        assert_eq!(changed, deserialized);
285    }
286
287    #[test]
288    fn test_node_lifecycle_action_serialization() {
289        let actions = vec![
290            NodeLifecycleAction::Start,
291            NodeLifecycleAction::Stop,
292            NodeLifecycleAction::Kill,
293            NodeLifecycleAction::Restart,
294        ];
295
296        for action in actions {
297            let json = serde_json::to_string(&action).unwrap();
298            let deserialized: NodeLifecycleAction = serde_json::from_str(&json).unwrap();
299            assert_eq!(action, deserialized);
300        }
301    }
302
303    #[test]
304    fn test_node_lifecycle_status_serialization() {
305        let statuses = vec![
306            NodeLifecycleStatus::Starting,
307            NodeLifecycleStatus::Started,
308            NodeLifecycleStatus::Running,
309            NodeLifecycleStatus::Stopping,
310            NodeLifecycleStatus::Stopped,
311            NodeLifecycleStatus::Failed,
312        ];
313
314        for status in statuses {
315            let json = serde_json::to_string(&status).unwrap();
316            let deserialized: NodeLifecycleStatus = serde_json::from_str(&json).unwrap();
317            assert_eq!(status, deserialized);
318        }
319    }
320
321    #[test]
322    fn test_topic_helpers() {
323        assert_eq!(topic_node_lifecycle("test-node"), "system/node/test-node/lifecycle");
324        assert_eq!(topic_node_status("test-node"), "system/node/test-node/status");
325    }
326
327    #[test]
328    fn test_default_graceful_timeout() {
329        let event = NodeLifecycleEvent {
330            node: "test".to_string(),
331            action: NodeLifecycleAction::Stop,
332            reason: "test".to_string(),
333            target_mode: None,
334            graceful_timeout_ms: default_graceful_timeout(),
335            config: None,
336        };
337
338        assert_eq!(event.graceful_timeout_ms, 5000);
339    }
340
341    #[test]
342    fn test_get_timestamp_ms() {
343        let ts1 = get_timestamp_ms();
344        std::thread::sleep(std::time::Duration::from_millis(10));
345        let ts2 = get_timestamp_ms();
346
347        assert!(ts2 > ts1);
348        assert!(ts2 - ts1 >= 10);
349    }
350
351    #[test]
352    fn test_node_status_event_optional_fields() {
353        // Without optional fields
354        let event1 = NodeStatusEvent {
355            node: "test".to_string(),
356            status: NodeLifecycleStatus::Starting,
357            pid: None,
358            error: None,
359            timestamp: 1234567890,
360        };
361
362        let json1 = serde_json::to_string(&event1).unwrap();
363        assert!(!json1.contains("\"pid\""));
364        assert!(!json1.contains("\"error\""));
365
366        // With optional fields
367        let event2 = NodeStatusEvent {
368            node: "test".to_string(),
369            status: NodeLifecycleStatus::Running,
370            pid: Some(12345),
371            error: Some("test error".to_string()),
372            timestamp: 1234567890,
373        };
374
375        let json2 = serde_json::to_string(&event2).unwrap();
376        assert!(json2.contains("\"pid\""));
377        assert!(json2.contains("\"error\""));
378    }
379}