Skip to main content

mecha10_core/context/
lifecycle.rs

1//! Context construction, instance naming, shutdown signaling, and logging span.
2
3use super::Context;
4#[cfg(feature = "messaging")]
5use crate::error::Mecha10Error;
6use crate::error::Result;
7#[cfg(feature = "messaging")]
8use anyhow::Context as _;
9use std::sync::Arc;
10use tokio::sync::RwLock;
11
12impl Context {
13    /// Create a new context
14    ///
15    /// # Arguments
16    ///
17    /// * `node_id` - Unique identifier for this node (fallback if NODE_NAME env var not set)
18    ///
19    /// # Environment Variables
20    ///
21    /// * `NODE_NAME` - If set, overrides the `node_id` parameter. This allows node-runner
22    ///   to set full identifiers like `@mecha10/speaker` for consistent naming across
23    ///   the CLI, dashboard, and health reporting.
24    ///
25    /// # Example
26    ///
27    /// ```rust
28    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
29    /// let ctx = Context::new("my-node").await?;
30    /// # Ok(())
31    /// # }
32    /// ```
33    pub async fn new(node_id: &str) -> Result<Self> {
34        // Use NODE_NAME from environment if set (for consistent naming with node-runner)
35        // This allows bundled nodes to report health with full identifiers like @mecha10/speaker
36        let node_id = std::env::var("NODE_NAME").unwrap_or_else(|_| node_id.to_string());
37        #[cfg(feature = "messaging")]
38        let (message_bus, control_plane_bus, redis_bridge) = {
39            // Load infrastructure config
40            let config = Self::load_infrastructure_config()?;
41
42            // Connect to local Redis
43            let redis_url = Self::get_redis_url_from_config(&config)?;
44            let local_bus = mecha10_messaging::MessageBus::connect(&redis_url, &node_id)
45                .await
46                .with_context(|| {
47                    format!(
48                        "Failed to connect local message bus for node '{}' to Redis at '{}'",
49                        node_id, redis_url
50                    )
51                })
52                .map_err(|e| Mecha10Error::MessagingError {
53                    message: format!("{:#}", e),
54                    suggestion: "Ensure local Redis is running at the configured URL".to_string(),
55                })?;
56
57            // Connect to control plane Redis if configured
58            let cp_bus = if let Some(cp_config) = &config.control_plane_redis {
59                let cp_url = &cp_config.url;
60                match mecha10_messaging::MessageBus::connect(cp_url, &node_id).await {
61                    Ok(bus) => {
62                        tracing::info!(
63                            "Connected to control plane Redis at '{}' for node '{}'",
64                            cp_url,
65                            node_id
66                        );
67                        Some(bus)
68                    }
69                    Err(e) => {
70                        tracing::warn!(
71                            "Failed to connect to control plane Redis at '{}': {}. Continuing with local Redis only.",
72                            cp_url,
73                            e
74                        );
75                        None
76                    }
77                }
78            } else {
79                None
80            };
81
82            // Initialize Redis bridge if both connections are available
83            let bridge = if let Some(_cp_bus_ref) = &cp_bus {
84                // Get bridge configuration (use default if not specified)
85                let bridge_config = config.bridge.unwrap_or_default();
86
87                if bridge_config.enabled {
88                    // Create bridge with separate clones of the buses
89                    let bridge_local_bus = mecha10_messaging::MessageBus::connect(&redis_url, &node_id)
90                        .await
91                        .map_err(|e| Mecha10Error::MessagingError {
92                            message: format!("Failed to create bridge connection to local Redis: {}", e),
93                            suggestion: "Check local Redis connection".to_string(),
94                        })?;
95
96                    let cp_url = config
97                        .control_plane_redis
98                        .as_ref()
99                        .map(|c| c.url.clone())
100                        .unwrap_or_default();
101                    let bridge_cp_bus = mecha10_messaging::MessageBus::connect(&cp_url, &node_id)
102                        .await
103                        .map_err(|e| Mecha10Error::MessagingError {
104                            message: format!("Failed to create bridge connection to control plane Redis: {}", e),
105                            suggestion: "Check control plane Redis connection".to_string(),
106                        })?;
107
108                    // Convert from infra_config::BridgeConfig to redis_bridge::BridgeConfig
109                    let redis_bridge_config = crate::redis_bridge::BridgeConfig {
110                        enabled: bridge_config.enabled,
111                        local_topics: bridge_config.local_topics,
112                        bridged_topics: bridge_config.bridged_topics,
113                        robot_id: bridge_config.robot_id,
114                    };
115
116                    let bridge =
117                        crate::redis_bridge::RedisBridge::new(redis_bridge_config, bridge_local_bus, bridge_cp_bus);
118
119                    // Start the bridge
120                    if let Err(e) = bridge.start().await {
121                        tracing::warn!("Failed to start Redis bridge: {}. Continuing without bridge.", e);
122                        None
123                    } else {
124                        tracing::info!("Redis bridge started successfully");
125                        Some(bridge)
126                    }
127                } else {
128                    tracing::debug!("Redis bridge is disabled in configuration");
129                    None
130                }
131            } else {
132                None
133            };
134
135            (local_bus, cp_bus, bridge)
136        };
137
138        Ok(Self {
139            node_id,
140            instance: None,
141            #[cfg(feature = "messaging")]
142            message_bus: Arc::new(RwLock::new(message_bus)),
143            #[cfg(feature = "messaging")]
144            control_plane_bus: Arc::new(RwLock::new(control_plane_bus)),
145            #[cfg(feature = "messaging")]
146            redis_bridge: Arc::new(RwLock::new(redis_bridge)),
147            shutdown: Arc::new(RwLock::new(false)),
148            state_manager: Arc::new(RwLock::new(None)),
149        })
150    }
151
152    /// Set an instance name for this context (builder pattern)
153    ///
154    /// This allows the same node implementation to run multiple instances
155    /// with different names. The node_id becomes "{node_name}/{instance}".
156    ///
157    /// # Example
158    ///
159    /// ```rust
160    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
161    /// // Single instance (no instance name)
162    /// let ctx = Context::new("camera_fake").await?;
163    /// // node_id: "camera_fake"
164    ///
165    /// // Multi-instance (with instance names)
166    /// let left_ctx = Context::new("camera_fake").await?.with_instance("left");
167    /// // node_id: "camera_fake/left"
168    ///
169    /// let right_ctx = Context::new("camera_fake").await?.with_instance("right");
170    /// // node_id: "camera_fake/right"
171    /// # Ok(())
172    /// # }
173    /// ```
174    pub fn with_instance(mut self, instance: &str) -> Self {
175        self.instance = Some(instance.to_string());
176        self.node_id = format!("{}/{}", self.node_id, instance);
177        self
178    }
179
180    /// Get the instance name if set
181    ///
182    /// Returns `None` for single-instance nodes, or `Some(instance)` for
183    /// multi-instance nodes.
184    pub fn instance(&self) -> Option<&str> {
185        self.instance.as_deref()
186    }
187
188    /// Get the node ID
189    pub fn node_id(&self) -> &str {
190        &self.node_id
191    }
192
193    /// Check if control plane Redis connection is available
194    #[cfg(feature = "messaging")]
195    pub async fn has_control_plane(&self) -> bool {
196        self.control_plane_bus.read().await.is_some()
197    }
198
199    /// Check if shutdown has been requested
200    pub async fn is_shutdown(&self) -> bool {
201        *self.shutdown.read().await
202    }
203
204    /// Request shutdown
205    pub async fn shutdown(&self) {
206        *self.shutdown.write().await = true;
207
208        // Stop Redis bridge if running
209        #[cfg(feature = "messaging")]
210        {
211            let bridge_opt = self.redis_bridge.read().await;
212            if let Some(bridge) = bridge_opt.as_ref() {
213                if let Err(e) = bridge.stop().await {
214                    tracing::warn!("Failed to stop Redis bridge during shutdown: {}", e);
215                }
216            }
217        }
218    }
219
220    /// Wait for shutdown signal
221    pub async fn wait_for_shutdown(&self) {
222        loop {
223            if self.is_shutdown().await {
224                break;
225            }
226            tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
227        }
228    }
229
230    /// Enter a logging span with this node's context
231    ///
232    /// This returns a guard that, when entered, adds the node_id to all log messages.
233    /// The span is automatically exited when the guard is dropped.
234    ///
235    /// # Example
236    ///
237    /// ```rust
238    /// use mecha10::prelude::*;
239    ///
240    /// # async fn example(ctx: &Context) -> Result<()> {
241    /// let _guard = ctx.logging_span().entered();
242    /// info!("This message includes node_id automatically");
243    /// debug!("So does this one");
244    /// # Ok(())
245    /// # }
246    /// ```
247    pub fn logging_span(&self) -> tracing::Span {
248        tracing::info_span!("node", node_id = %self.node_id)
249    }
250}