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    /// # use mecha10_core::Context;
29    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
30    /// let ctx = Context::new("my-node").await?;
31    /// # Ok(())
32    /// # }
33    /// ```
34    pub async fn new(node_id: &str) -> Result<Self> {
35        let node_id = std::env::var("NODE_NAME").unwrap_or_else(|_| node_id.to_string());
36
37        // Multi-instance support (LAB-4131): when node-runner spawns a node with an
38        // `instance_id` (from `mecha10.json`'s `{"name": ..., "instance_id": "real"}` node-list
39        // entry shape), it sets `NODE_INSTANCE_ID` on the child process's environment instead of
40        // baking the instance into `NODE_NAME` itself, so this applies uniformly to every node
41        // via `Context::new` - no per-node opt-in required. This has the same effect as an
42        // explicit `.with_instance(..)` call (see below): `node_id` becomes
43        // "{node_id}/{instance}" and `Self::instance` is set, so config loading
44        // (`load_node_config`) and health reporting both pick it up automatically.
45        let instance = std::env::var("NODE_INSTANCE_ID").ok().filter(|s| !s.is_empty());
46        let node_id = match &instance {
47            Some(instance) => format!("{}/{}", node_id, instance),
48            None => node_id,
49        };
50        #[cfg(feature = "messaging")]
51        let (message_bus, control_plane_bus, redis_bridge) = {
52            let config = Self::load_infrastructure_config()?;
53
54            let redis_url = Self::get_redis_url_from_config(&config)?;
55            let local_bus = mecha10_messaging::MessageBus::connect(&redis_url, &node_id)
56                .await
57                .with_context(|| {
58                    format!(
59                        "Failed to connect local message bus for node '{}' to Redis at '{}'",
60                        node_id, redis_url
61                    )
62                })
63                .map_err(|e| Mecha10Error::MessagingError {
64                    message: format!("{:#}", e),
65                    suggestion: "Ensure local Redis is running at the configured URL".to_string(),
66                })?;
67
68            let cp_bus = if let Some(cp_config) = &config.control_plane_redis {
69                let cp_url = &cp_config.url;
70                match mecha10_messaging::MessageBus::connect(cp_url, &node_id).await {
71                    Ok(bus) => {
72                        tracing::info!(
73                            "Connected to control plane Redis at '{}' for node '{}'",
74                            cp_url,
75                            node_id
76                        );
77                        Some(bus)
78                    }
79                    Err(e) => {
80                        tracing::warn!(
81                            "Failed to connect to control plane Redis at '{}': {}. Continuing with local Redis only.",
82                            cp_url,
83                            e
84                        );
85                        None
86                    }
87                }
88            } else {
89                None
90            };
91
92            let bridge = if let Some(_cp_bus_ref) = &cp_bus {
93                let bridge_config = config.bridge.unwrap_or_default();
94
95                if bridge_config.enabled {
96                    let bridge_local_bus = mecha10_messaging::MessageBus::connect(&redis_url, &node_id)
97                        .await
98                        .map_err(|e| Mecha10Error::MessagingError {
99                            message: format!("Failed to create bridge connection to local Redis: {}", e),
100                            suggestion: "Check local Redis connection".to_string(),
101                        })?;
102
103                    let cp_url = config
104                        .control_plane_redis
105                        .as_ref()
106                        .map(|c| c.url.clone())
107                        .unwrap_or_default();
108                    let bridge_cp_bus = mecha10_messaging::MessageBus::connect(&cp_url, &node_id)
109                        .await
110                        .map_err(|e| Mecha10Error::MessagingError {
111                            message: format!("Failed to create bridge connection to control plane Redis: {}", e),
112                            suggestion: "Check control plane Redis connection".to_string(),
113                        })?;
114
115                    let redis_bridge_config = crate::redis_bridge::BridgeConfig {
116                        enabled: bridge_config.enabled,
117                        local_topics: bridge_config.local_topics,
118                        bridged_topics: bridge_config.bridged_topics,
119                        robot_id: bridge_config.robot_id,
120                    };
121
122                    let bridge =
123                        crate::redis_bridge::RedisBridge::new(redis_bridge_config, bridge_local_bus, bridge_cp_bus);
124
125                    if let Err(e) = bridge.start().await {
126                        tracing::warn!("Failed to start Redis bridge: {}. Continuing without bridge.", e);
127                        None
128                    } else {
129                        tracing::info!("Redis bridge started successfully");
130                        Some(bridge)
131                    }
132                } else {
133                    tracing::debug!("Redis bridge is disabled in configuration");
134                    None
135                }
136            } else {
137                None
138            };
139
140            (local_bus, cp_bus, bridge)
141        };
142
143        Ok(Self {
144            node_id,
145            instance,
146            #[cfg(feature = "messaging")]
147            message_bus: Arc::new(RwLock::new(message_bus)),
148            #[cfg(feature = "messaging")]
149            control_plane_bus: Arc::new(RwLock::new(control_plane_bus)),
150            #[cfg(feature = "messaging")]
151            redis_bridge: Arc::new(RwLock::new(redis_bridge)),
152            shutdown: Arc::new(RwLock::new(false)),
153            state_manager: Arc::new(RwLock::new(None)),
154            blackboard: crate::blackboard::Blackboard::new(),
155        })
156    }
157
158    /// Set an instance name for this context (builder pattern)
159    ///
160    /// This allows the same node implementation to run multiple instances
161    /// with different names. The node_id becomes "{node_name}/{instance}".
162    ///
163    /// # Example
164    ///
165    /// ```rust
166    /// # use mecha10_core::Context;
167    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
168    /// // Single instance (no instance name)
169    /// let ctx = Context::new("camera_fake").await?;
170    /// // node_id: "camera_fake"
171    ///
172    /// // Multi-instance (with instance names)
173    /// let left_ctx = Context::new("camera_fake").await?.with_instance("left");
174    /// // node_id: "camera_fake/left"
175    ///
176    /// let right_ctx = Context::new("camera_fake").await?.with_instance("right");
177    /// // node_id: "camera_fake/right"
178    /// # Ok(())
179    /// # }
180    /// ```
181    pub fn with_instance(mut self, instance: &str) -> Self {
182        self.instance = Some(instance.to_string());
183        self.node_id = format!("{}/{}", self.node_id, instance);
184        self
185    }
186
187    /// Get the instance name if set
188    ///
189    /// Returns `None` for single-instance nodes, or `Some(instance)` for
190    /// multi-instance nodes.
191    pub fn instance(&self) -> Option<&str> {
192        self.instance.as_deref()
193    }
194
195    /// Get the node ID
196    pub fn node_id(&self) -> &str {
197        &self.node_id
198    }
199
200    /// Check if control plane Redis connection is available
201    #[cfg(feature = "messaging")]
202    pub async fn has_control_plane(&self) -> bool {
203        self.control_plane_bus.read().await.is_some()
204    }
205
206    /// Check if shutdown has been requested
207    pub async fn is_shutdown(&self) -> bool {
208        *self.shutdown.read().await
209    }
210
211    /// Request shutdown
212    pub async fn shutdown(&self) {
213        *self.shutdown.write().await = true;
214
215        #[cfg(feature = "messaging")]
216        {
217            let bridge_opt = self.redis_bridge.read().await;
218            if let Some(bridge) = bridge_opt.as_ref() {
219                if let Err(e) = bridge.stop().await {
220                    tracing::warn!("Failed to stop Redis bridge during shutdown: {}", e);
221                }
222            }
223        }
224    }
225
226    /// Wait for shutdown signal
227    pub async fn wait_for_shutdown(&self) {
228        loop {
229            if self.is_shutdown().await {
230                break;
231            }
232            tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
233        }
234    }
235
236    /// Enter a logging span with this node's context
237    ///
238    /// This returns a guard that, when entered, adds the node_id to all log messages.
239    /// The span is automatically exited when the guard is dropped.
240    ///
241    /// # Example
242    ///
243    /// ```rust
244    /// use mecha10_core::prelude::*;
245    ///
246    /// # async fn example(ctx: &Context) -> Result<()> {
247    /// let _guard = ctx.logging_span().entered();
248    /// info!("This message includes node_id automatically");
249    /// debug!("So does this one");
250    /// # Ok(())
251    /// # }
252    /// ```
253    pub fn logging_span(&self) -> tracing::Span {
254        tracing::info_span!("node", node_id = %self.node_id)
255    }
256}