mecha10-core 0.6.2

Mecha10 core runtime and foundational types
Documentation
//! Lifecycle event schemas for mode-based node management
//!
//! This module defines the event types used for dynamic lifecycle management:
//! - Mode change requests and confirmations
//! - Node lifecycle events
//! - Node status reporting
//!
//! # Event Flow
//!
//! ```text
//! CLI → ModeRequest → Runtime Lifecycle → Supervisor → Nodes
//!                                       ↓
//!                                 ModeChanged
//! ```
//!
//! # Example
//!
//! ```rust,ignore
//! use mecha10_core::lifecycle::*;
//!
//! // Request mode change
//! context.publish(
//!     TOPIC_MODE_REQUEST,
//!     &ModeRequest {
//!         mode: "simulation".to_string(),
//!         requested_by: "cli".to_string(),
//!         timestamp: get_timestamp_ms(),
//!     }
//! ).await?;
//!
//! // Listen for mode changes
//! let mut sub = context.subscribe(TOPIC_MODE_CHANGED).await?;
//! while let Some(msg) = sub.next().await {
//!     let event: ModeChanged = msg.decode()?;
//!     println!("Mode changed to: {}", event.current_mode);
//! }
//! ```

use serde::{Deserialize, Serialize};

// ========== Mode Management Events ==========

/// Request to change operational mode
///
/// Published by CLI or nodes to request a mode transition.
/// Runtime lifecycle manager subscribes to these requests.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ModeRequest {
    /// Target mode name (e.g., "simulation", "teleop", "autonomous")
    pub mode: String,

    /// Who requested the change (e.g., "cli", "mission-controller")
    pub requested_by: String,

    /// Unix timestamp in milliseconds
    pub timestamp: u64,
}

/// Notification that mode has changed successfully
///
/// Published by runtime lifecycle manager after mode transition completes.
/// Nodes can subscribe to react to mode changes.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ModeChanged {
    /// Previous mode name
    pub previous_mode: String,

    /// New current mode name
    pub current_mode: String,

    /// Nodes that were started during transition
    pub nodes_started: Vec<String>,

    /// Nodes that were stopped during transition
    pub nodes_stopped: Vec<String>,

    /// Unix timestamp in milliseconds
    pub timestamp: u64,
}

/// Error during mode transition
///
/// Published by runtime lifecycle manager if mode change fails.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ModeError {
    /// Requested mode that failed
    pub mode: String,

    /// Error message
    pub error: String,

    /// Current mode (unchanged)
    pub current_mode: String,

    /// Unix timestamp in milliseconds
    pub timestamp: u64,
}

// ========== Node Lifecycle Events ==========

/// Lifecycle action for a specific node (internal to runtime)
///
/// Note: These events are primarily for internal runtime use.
/// Most users should use mode requests instead.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct NodeLifecycleEvent {
    /// Node name
    pub node: String,

    /// Action to perform
    pub action: NodeLifecycleAction,

    /// Reason for action
    pub reason: String,

    /// Target mode context (if mode change)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub target_mode: Option<String>,

    /// Graceful shutdown timeout in milliseconds
    #[serde(default = "default_graceful_timeout")]
    pub graceful_timeout_ms: u64,

    /// Node-specific configuration
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub config: Option<serde_json::Value>,
}

/// Actions that can be performed on a node
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum NodeLifecycleAction {
    /// Start the node
    Start,

    /// Stop the node gracefully
    Stop,

    /// Force kill the node (if graceful stop times out)
    Kill,

    /// Restart the node (stop then start)
    Restart,
}

/// Node status report
///
/// Published by runtime supervisor to report node state changes.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct NodeStatusEvent {
    /// Node name
    pub node: String,

    /// Current status
    pub status: NodeLifecycleStatus,

    /// Process ID (if running)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub pid: Option<u32>,

    /// Error message (if failed)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,

    /// Unix timestamp in milliseconds
    pub timestamp: u64,
}

/// Lifecycle status of a node
///
/// This represents the operational state of a node process.
/// Note: This is different from `discovery::NodeStatus` which is for service discovery.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum NodeLifecycleStatus {
    /// Node is starting up
    Starting,

    /// Node successfully started
    Started,

    /// Node is running normally
    Running,

    /// Node is shutting down
    Stopping,

    /// Node has stopped
    Stopped,

    /// Node failed to start or crashed
    Failed,
}

// ========== Topic Constants ==========

/// Topic for mode change requests
///
/// Format: `system/lifecycle/mode/request`
///
/// Payload: [`ModeRequest`]
pub const TOPIC_MODE_REQUEST: &str = "system/lifecycle/mode/request";

/// Topic for mode change confirmations
///
/// Format: `system/lifecycle/mode/changed`
///
/// Payload: [`ModeChanged`]
pub const TOPIC_MODE_CHANGED: &str = "system/lifecycle/mode/changed";

/// Topic for mode change errors
///
/// Format: `system/lifecycle/mode/error`
///
/// Payload: [`ModeError`]
pub const TOPIC_MODE_ERROR: &str = "system/lifecycle/mode/error";

/// Get topic for node lifecycle events (internal)
///
/// Format: `system/node/{node}/lifecycle`
///
/// Payload: [`NodeLifecycleEvent`]
///
/// Note: Primarily for internal runtime use
pub fn topic_node_lifecycle(node: &str) -> String {
    format!("system/node/{}/lifecycle", node)
}

/// Get topic for node status events
///
/// Format: `system/node/{node}/status`
///
/// Payload: [`NodeStatusEvent`]
pub fn topic_node_status(node: &str) -> String {
    format!("system/node/{}/status", node)
}

// ========== Helper Functions ==========

/// Default graceful shutdown timeout (5 seconds)
fn default_graceful_timeout() -> u64 {
    5000
}

/// Get current timestamp in milliseconds since Unix epoch
pub fn get_timestamp_ms() -> u64 {
    crate::prelude::now_millis()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_mode_request_serialization() {
        let request = ModeRequest {
            mode: "simulation".to_string(),
            requested_by: "cli".to_string(),
            timestamp: 1234567890,
        };

        let json = serde_json::to_string(&request).unwrap();
        let deserialized: ModeRequest = serde_json::from_str(&json).unwrap();

        assert_eq!(request, deserialized);
    }

    #[test]
    fn test_mode_changed_serialization() {
        let changed = ModeChanged {
            previous_mode: "startup".to_string(),
            current_mode: "simulation".to_string(),
            nodes_started: vec!["sim-bridge".to_string(), "object-detector".to_string()],
            nodes_stopped: vec!["imu".to_string(), "motor".to_string()],
            timestamp: 1234567890,
        };

        let json = serde_json::to_string(&changed).unwrap();
        let deserialized: ModeChanged = serde_json::from_str(&json).unwrap();

        assert_eq!(changed, deserialized);
    }

    #[test]
    fn test_node_lifecycle_action_serialization() {
        let actions = vec![
            NodeLifecycleAction::Start,
            NodeLifecycleAction::Stop,
            NodeLifecycleAction::Kill,
            NodeLifecycleAction::Restart,
        ];

        for action in actions {
            let json = serde_json::to_string(&action).unwrap();
            let deserialized: NodeLifecycleAction = serde_json::from_str(&json).unwrap();
            assert_eq!(action, deserialized);
        }
    }

    #[test]
    fn test_node_lifecycle_status_serialization() {
        let statuses = vec![
            NodeLifecycleStatus::Starting,
            NodeLifecycleStatus::Started,
            NodeLifecycleStatus::Running,
            NodeLifecycleStatus::Stopping,
            NodeLifecycleStatus::Stopped,
            NodeLifecycleStatus::Failed,
        ];

        for status in statuses {
            let json = serde_json::to_string(&status).unwrap();
            let deserialized: NodeLifecycleStatus = serde_json::from_str(&json).unwrap();
            assert_eq!(status, deserialized);
        }
    }

    #[test]
    fn test_topic_helpers() {
        assert_eq!(topic_node_lifecycle("test-node"), "system/node/test-node/lifecycle");
        assert_eq!(topic_node_status("test-node"), "system/node/test-node/status");
    }

    #[test]
    fn test_default_graceful_timeout() {
        let event = NodeLifecycleEvent {
            node: "test".to_string(),
            action: NodeLifecycleAction::Stop,
            reason: "test".to_string(),
            target_mode: None,
            graceful_timeout_ms: default_graceful_timeout(),
            config: None,
        };

        assert_eq!(event.graceful_timeout_ms, 5000);
    }

    #[test]
    fn test_get_timestamp_ms() {
        let ts1 = get_timestamp_ms();
        std::thread::sleep(std::time::Duration::from_millis(10));
        let ts2 = get_timestamp_ms();

        assert!(ts2 > ts1);
        assert!(ts2 - ts1 >= 10);
    }

    #[test]
    fn test_node_status_event_optional_fields() {
        // Without optional fields
        let event1 = NodeStatusEvent {
            node: "test".to_string(),
            status: NodeLifecycleStatus::Starting,
            pid: None,
            error: None,
            timestamp: 1234567890,
        };

        let json1 = serde_json::to_string(&event1).unwrap();
        assert!(!json1.contains("\"pid\""));
        assert!(!json1.contains("\"error\""));

        // With optional fields
        let event2 = NodeStatusEvent {
            node: "test".to_string(),
            status: NodeLifecycleStatus::Running,
            pid: Some(12345),
            error: Some("test error".to_string()),
            timestamp: 1234567890,
        };

        let json2 = serde_json::to_string(&event2).unwrap();
        assert!(json2.contains("\"pid\""));
        assert!(json2.contains("\"error\""));
    }
}