Skip to main content

rust_supervisor/runtime/
supervisor.rs

1//! Runtime supervisor entry point.
2//!
3//! This module validates supervisor declarations, derives runtime options, and
4//! returns a [`crate::control::handle::SupervisorHandle`].
5
6use crate::config::state::ConfigState;
7use crate::control::handle::SupervisorHandle;
8#[cfg(unix)]
9use crate::dashboard::{
10    config::validate_dashboard_ipc_config, error::DashboardError,
11    runtime::start_dashboard_ipc_runtime,
12};
13use crate::error::types::SupervisorError;
14use crate::observe::pipeline::ObservabilityPipeline;
15use crate::runtime::control_loop::{RuntimeControlState, run_control_loop};
16use crate::runtime::lifecycle::RuntimeControlPlane;
17use crate::runtime::watchdog::RuntimeWatchdog;
18use crate::spec::supervisor::SupervisorSpec;
19use crate::task::factory_registry::TaskFactoryRegistry;
20use std::path::Path;
21use std::sync::{Arc, Mutex};
22use tokio::sync::{broadcast, mpsc};
23
24/// Supervisor runtime entry point.
25#[derive(Debug, Clone, Copy, Default)]
26pub struct Supervisor;
27
28impl Supervisor {
29    /// Starts a supervisor runtime from an owned specification value.
30    ///
31    /// # Arguments
32    ///
33    /// - `spec`: Supervisor specification owned by the caller.
34    ///
35    /// # Returns
36    ///
37    /// Returns a [`SupervisorHandle`] connected to the runtime control loop.
38    pub async fn start(spec: SupervisorSpec) -> Result<SupervisorHandle, SupervisorError> {
39        Self::start_with_factory_registry(spec, TaskFactoryRegistry::new()).await
40    }
41
42    /// Starts a supervisor runtime from validated configuration state.
43    ///
44    /// # Arguments
45    ///
46    /// - `state`: Validated configuration state owned by the caller.
47    ///
48    /// # Returns
49    ///
50    /// Returns a [`SupervisorHandle`] only after configuration has produced a
51    /// valid supervisor specification.
52    pub async fn start_from_config_state(
53        state: ConfigState,
54    ) -> Result<SupervisorHandle, SupervisorError> {
55        #[cfg(unix)]
56        let audit_config = state.audit.clone();
57        #[cfg(unix)]
58        let dashboard_config = state.dashboard.clone();
59        let spec = state.to_supervisor_spec()?;
60        #[cfg(unix)]
61        let handle = Self::start(spec.clone()).await?;
62        #[cfg(not(unix))]
63        let handle = Self::start(spec).await?;
64        #[cfg(unix)]
65        let handle = if let Some(dashboard_config) =
66            validate_dashboard_ipc_config(dashboard_config.as_ref())
67                .map_err(dashboard_startup_error)?
68        {
69            let dashboard_runtime =
70                start_dashboard_ipc_runtime(dashboard_config, audit_config, spec, handle.clone())
71                    .map_err(dashboard_startup_error)?;
72            handle.with_dashboard_runtime(dashboard_runtime)
73        } else {
74            handle
75        };
76        Ok(handle)
77    }
78
79    /// Starts a supervisor runtime from validated configuration and a factory registry.
80    ///
81    /// # Arguments
82    ///
83    /// - `state`: Validated configuration state owned by the caller.
84    /// - `task_factory_registry`: Registry used to bind worker `factory_key` values.
85    ///
86    /// # Returns
87    ///
88    /// Returns a [`SupervisorHandle`] only after configuration has produced a
89    /// valid supervisor specification with executable worker factories.
90    pub async fn start_from_config_state_with_factories(
91        state: ConfigState,
92        task_factory_registry: TaskFactoryRegistry,
93    ) -> Result<SupervisorHandle, SupervisorError> {
94        #[cfg(unix)]
95        let audit_config = state.audit.clone();
96        #[cfg(unix)]
97        let dashboard_config = state.dashboard.clone();
98        let spec = state.to_supervisor_spec_with_factories(&task_factory_registry)?;
99        #[cfg(unix)]
100        let handle =
101            Self::start_with_factory_registry(spec.clone(), task_factory_registry.clone()).await?;
102        #[cfg(not(unix))]
103        let handle = Self::start_with_factory_registry(spec, task_factory_registry).await?;
104        #[cfg(unix)]
105        let handle = if let Some(dashboard_config) =
106            validate_dashboard_ipc_config(dashboard_config.as_ref())
107                .map_err(dashboard_startup_error)?
108        {
109            let dashboard_runtime =
110                start_dashboard_ipc_runtime(dashboard_config, audit_config, spec, handle.clone())
111                    .map_err(dashboard_startup_error)?;
112            handle.with_dashboard_runtime(dashboard_runtime)
113        } else {
114            handle
115        };
116        Ok(handle)
117    }
118
119    /// Starts a supervisor runtime from a YAML configuration file.
120    ///
121    /// # Arguments
122    ///
123    /// - `path`: Path to the YAML configuration file.
124    ///
125    /// # Returns
126    ///
127    /// Returns a [`SupervisorHandle`] only after the configuration file has
128    /// loaded and validated successfully.
129    pub async fn start_from_config_file(
130        path: impl AsRef<Path>,
131    ) -> Result<SupervisorHandle, SupervisorError> {
132        let state = crate::config::loader::load_config_from_yaml_file(path)?;
133        Self::start_from_config_state(state).await
134    }
135
136    /// Starts a supervisor runtime from a YAML file and a factory registry.
137    ///
138    /// # Arguments
139    ///
140    /// - `path`: Path to the YAML configuration file.
141    /// - `task_factory_registry`: Registry used to bind worker `factory_key` values.
142    ///
143    /// # Returns
144    ///
145    /// Returns a [`SupervisorHandle`] after configuration and factory binding succeed.
146    pub async fn start_from_config_file_with_factories(
147        path: impl AsRef<Path>,
148        task_factory_registry: TaskFactoryRegistry,
149    ) -> Result<SupervisorHandle, SupervisorError> {
150        let state = crate::config::loader::load_config_from_yaml_file(path)?;
151        Self::start_from_config_state_with_factories(state, task_factory_registry).await
152    }
153
154    /// Starts a supervisor runtime with an explicit task factory registry.
155    ///
156    /// # Arguments
157    ///
158    /// - `spec`: Supervisor specification owned by the caller.
159    /// - `task_factory_registry`: Registry used by dynamic child declarations.
160    ///
161    /// # Returns
162    ///
163    /// Returns a [`SupervisorHandle`] connected to the runtime control loop.
164    pub async fn start_with_factory_registry(
165        spec: SupervisorSpec,
166        task_factory_registry: TaskFactoryRegistry,
167    ) -> Result<SupervisorHandle, SupervisorError> {
168        spec.validate()?;
169        let backpressure_config = spec.backpressure_config.clone();
170        let (command_sender, command_receiver) = mpsc::channel(spec.control_channel_capacity);
171        let (event_sender, _) = broadcast::channel(spec.event_channel_capacity);
172        let control_plane = RuntimeControlPlane::new();
173        let observability = Arc::new(Mutex::new(ObservabilityPipeline::with_backpressure_config(
174            spec.event_channel_capacity,
175            spec.event_channel_capacity,
176            spec.metrics_enabled,
177            spec.audit_enabled,
178            backpressure_config,
179        )));
180        let state = RuntimeControlState::new_with_factory_registry(
181            spec,
182            command_sender.clone(),
183            observability.clone(),
184            task_factory_registry,
185        )?;
186        let join_handle = tokio::spawn(run_control_loop(
187            state,
188            command_receiver,
189            event_sender.clone(),
190        ));
191        RuntimeWatchdog::publish_started(control_plane.clone(), event_sender.clone());
192        RuntimeWatchdog::spawn(control_plane.clone(), join_handle, event_sender.clone());
193        Ok(SupervisorHandle::new_with_observability(
194            command_sender,
195            event_sender,
196            control_plane,
197            observability,
198        ))
199    }
200}
201
202/// Converts dashboard startup failures into supervisor startup errors.
203#[cfg(unix)]
204fn dashboard_startup_error(error: DashboardError) -> SupervisorError {
205    SupervisorError::fatal_config(format!("dashboard IPC startup failed: {error}"))
206}