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