Skip to main content

rust_supervisor/config/
state.rs

1//! Immutable configuration state for supervisor runtime values.
2//!
3//! Raw YAML input belongs to [`crate::config::configurable`]. This module owns
4//! semantic validation and conversion into supervisor runtime declarations.
5
6use crate::config::configurable::{
7    DashboardIpcConfig, ObservabilityConfig, PolicyConfig, ShutdownConfig, SupervisorConfig,
8    SupervisorRootConfig,
9};
10use serde::{Deserialize, Serialize};
11use std::time::Duration;
12
13/// Immutable validated configuration state.
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15pub struct ConfigState {
16    /// Root supervisor declaration values.
17    pub supervisor: SupervisorRootConfig,
18    /// Runtime policy values.
19    pub policy: PolicyConfig,
20    /// Shutdown budget values.
21    pub shutdown: ShutdownConfig,
22    /// Observability switches and capacities.
23    pub observability: ObservabilityConfig,
24    /// Optional target-side dashboard IPC configuration.
25    pub ipc: Option<DashboardIpcConfig>,
26}
27
28impl TryFrom<SupervisorConfig> for ConfigState {
29    type Error = crate::error::types::SupervisorError;
30
31    /// Converts a deserialized supervisor config into validated state.
32    fn try_from(config: SupervisorConfig) -> Result<Self, Self::Error> {
33        validate_policy(&config.policy)?;
34        validate_shutdown(&config.shutdown)?;
35        validate_observability(&config.observability)?;
36        validate_ipc(config.ipc.as_ref())?;
37        Ok(Self {
38            supervisor: config.supervisor,
39            policy: config.policy,
40            shutdown: config.shutdown,
41            observability: config.observability,
42            ipc: config.ipc,
43        })
44    }
45}
46
47impl ConfigState {
48    /// Converts validated configuration into a supervisor declaration.
49    ///
50    /// # Arguments
51    ///
52    /// This function has no arguments.
53    ///
54    /// # Returns
55    ///
56    /// Returns a [`crate::spec::supervisor::SupervisorSpec`] derived from the
57    /// validated YAML configuration.
58    ///
59    /// # Examples
60    ///
61    /// ```
62    /// let yaml = r#"
63    /// supervisor:
64    ///   strategy: OneForAll
65    /// policy:
66    ///   child_restart_limit: 10
67    ///   child_restart_window_ms: 60000
68    ///   supervisor_failure_limit: 30
69    ///   supervisor_failure_window_ms: 60000
70    ///   initial_backoff_ms: 10
71    ///   max_backoff_ms: 1000
72    ///   jitter_ratio: 0.0
73    ///   heartbeat_interval_ms: 1000
74    ///   stale_after_ms: 3000
75    /// shutdown:
76    ///   graceful_timeout_ms: 1000
77    ///   abort_wait_ms: 100
78    /// observability:
79    ///   event_journal_capacity: 64
80    ///   metrics_enabled: true
81    ///   audit_enabled: true
82    /// "#;
83    /// let state = rust_supervisor::config::yaml::parse_config_state(yaml).unwrap();
84    /// let spec = state.to_supervisor_spec().unwrap();
85    /// assert_eq!(spec.strategy, rust_supervisor::spec::supervisor::SupervisionStrategy::OneForAll);
86    /// assert_eq!(spec.supervisor_failure_limit, 30);
87    /// ```
88    pub fn to_supervisor_spec(
89        &self,
90    ) -> Result<crate::spec::supervisor::SupervisorSpec, crate::error::types::SupervisorError> {
91        let mut spec = crate::spec::supervisor::SupervisorSpec::root(Vec::new());
92        spec.strategy = self.supervisor.strategy;
93        spec.config_version = self.config_version();
94        spec.supervisor_failure_limit = self.policy.supervisor_failure_limit;
95        spec.control_channel_capacity = self.observability.event_journal_capacity;
96        spec.event_channel_capacity = self.observability.event_journal_capacity;
97        spec.default_backoff_policy = crate::spec::child::BackoffPolicy::new(
98            Duration::from_millis(self.policy.initial_backoff_ms),
99            Duration::from_millis(self.policy.max_backoff_ms),
100            self.policy.jitter_ratio,
101        );
102        spec.default_health_policy = crate::spec::child::HealthPolicy::new(
103            Duration::from_millis(self.policy.heartbeat_interval_ms),
104            Duration::from_millis(self.policy.stale_after_ms),
105        );
106        spec.default_shutdown_policy = crate::spec::child::ShutdownPolicy::new(
107            Duration::from_millis(self.shutdown.graceful_timeout_ms),
108            Duration::from_millis(self.shutdown.abort_wait_ms),
109        );
110        spec.validate()?;
111        Ok(spec)
112    }
113
114    /// Builds a stable configuration version string from configured values.
115    ///
116    /// # Arguments
117    ///
118    /// This function has no arguments.
119    ///
120    /// # Returns
121    ///
122    /// Returns a deterministic version string for diagnostics.
123    fn config_version(&self) -> String {
124        format!(
125            "supervisor-{:?}-policy-{}-{}-shutdown-{}-observe-{}",
126            self.supervisor.strategy,
127            self.policy.child_restart_limit,
128            self.policy.supervisor_failure_limit,
129            self.shutdown.graceful_timeout_ms,
130            self.observability.event_journal_capacity
131        )
132    }
133}
134
135/// Validates policy configuration invariants.
136///
137/// # Arguments
138///
139/// - `policy`: Policy configuration loaded from YAML.
140///
141/// # Returns
142///
143/// Returns `Ok(())` when policy values are usable.
144fn validate_policy(policy: &PolicyConfig) -> Result<(), crate::error::types::SupervisorError> {
145    validate_positive(policy.child_restart_limit, "policy.child_restart_limit")?;
146    validate_positive(
147        policy.supervisor_failure_limit,
148        "policy.supervisor_failure_limit",
149    )?;
150    validate_positive(
151        policy.child_restart_window_ms,
152        "policy.child_restart_window_ms",
153    )?;
154    validate_positive(
155        policy.supervisor_failure_window_ms,
156        "policy.supervisor_failure_window_ms",
157    )?;
158    validate_positive(policy.initial_backoff_ms, "policy.initial_backoff_ms")?;
159    validate_positive(policy.max_backoff_ms, "policy.max_backoff_ms")?;
160    validate_positive(policy.heartbeat_interval_ms, "policy.heartbeat_interval_ms")?;
161    validate_positive(policy.stale_after_ms, "policy.stale_after_ms")?;
162    if policy.initial_backoff_ms > policy.max_backoff_ms {
163        return Err(crate::error::types::SupervisorError::fatal_config(
164            "policy.initial_backoff_ms must be less than or equal to policy.max_backoff_ms",
165        ));
166    }
167    if !(0.0..=1.0).contains(&policy.jitter_ratio) {
168        return Err(crate::error::types::SupervisorError::fatal_config(
169            "policy.jitter_ratio must be between 0 and 1",
170        ));
171    }
172    Ok(())
173}
174
175/// Validates shutdown configuration invariants.
176///
177/// # Arguments
178///
179/// - `shutdown`: Shutdown configuration loaded from YAML.
180///
181/// # Returns
182///
183/// Returns `Ok(())` when shutdown values are usable.
184fn validate_shutdown(
185    shutdown: &ShutdownConfig,
186) -> Result<(), crate::error::types::SupervisorError> {
187    validate_positive(shutdown.graceful_timeout_ms, "shutdown.graceful_timeout_ms")?;
188    validate_positive(shutdown.abort_wait_ms, "shutdown.abort_wait_ms")
189}
190
191/// Validates observability configuration invariants.
192///
193/// # Arguments
194///
195/// - `observability`: Observability configuration loaded from YAML.
196///
197/// # Returns
198///
199/// Returns `Ok(())` when observability values are usable.
200fn validate_observability(
201    observability: &ObservabilityConfig,
202) -> Result<(), crate::error::types::SupervisorError> {
203    validate_positive(
204        observability.event_journal_capacity as u64,
205        "observability.event_journal_capacity",
206    )
207}
208
209/// Validates dashboard IPC configuration invariants.
210///
211/// # Arguments
212///
213/// - `ipc`: Optional target-side dashboard IPC configuration.
214///
215/// # Returns
216///
217/// Returns `Ok(())` when IPC is absent, disabled, or semantically valid.
218fn validate_ipc(
219    ipc: Option<&DashboardIpcConfig>,
220) -> Result<(), crate::error::types::SupervisorError> {
221    crate::dashboard::config::validate_dashboard_ipc_config(ipc)
222        .map(|_| ())
223        .map_err(|error| crate::error::types::SupervisorError::fatal_config(error.to_string()))
224}
225
226/// Validates that a runtime configuration number is positive.
227///
228/// # Arguments
229///
230/// - `value`: Runtime configuration number.
231/// - `name`: Configuration key name.
232///
233/// # Returns
234///
235/// Returns `Ok(())` when the value is positive.
236fn validate_positive(
237    value: impl Into<u64>,
238    name: &str,
239) -> Result<(), crate::error::types::SupervisorError> {
240    if value.into() == 0 {
241        Err(crate::error::types::SupervisorError::fatal_config(format!(
242            "{name} must be greater than zero"
243        )))
244    } else {
245        Ok(())
246    }
247}