Skip to main content

rust_supervisor/config/
state.rs

1//! Immutable configuration state for supervisor runtime values.
2//!
3//! All runtime tunable values enter the system through this module after YAML
4//! loading and validation.
5
6use serde::{Deserialize, Serialize};
7use std::time::Duration;
8
9/// Configuration file shape loaded from YAML.
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub struct SupervisorConfig {
12    /// Root supervisor declaration values.
13    pub supervisor: SupervisorRootConfig,
14    /// Runtime policy values.
15    pub policy: PolicyConfig,
16    /// Shutdown budget values.
17    pub shutdown: ShutdownConfig,
18    /// Observability switches and capacities.
19    pub observability: ObservabilityConfig,
20}
21
22/// Immutable validated configuration state.
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24pub struct ConfigState {
25    /// Root supervisor declaration values.
26    pub supervisor: SupervisorRootConfig,
27    /// Runtime policy values.
28    pub policy: PolicyConfig,
29    /// Shutdown budget values.
30    pub shutdown: ShutdownConfig,
31    /// Observability switches and capacities.
32    pub observability: ObservabilityConfig,
33}
34
35/// Root supervisor configuration.
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37pub struct SupervisorRootConfig {
38    /// Restart scope strategy for child failures.
39    pub strategy: crate::spec::supervisor::SupervisionStrategy,
40}
41
42/// Restart, backoff, and fuse configuration.
43#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
44pub struct PolicyConfig {
45    /// Maximum child restarts within the child restart window.
46    pub child_restart_limit: u32,
47    /// Child restart window in milliseconds.
48    pub child_restart_window_ms: u64,
49    /// Maximum supervisor failures within the supervisor failure window.
50    pub supervisor_failure_limit: u32,
51    /// Supervisor failure window in milliseconds.
52    pub supervisor_failure_window_ms: u64,
53    /// Initial backoff in milliseconds.
54    pub initial_backoff_ms: u64,
55    /// Maximum backoff in milliseconds.
56    pub max_backoff_ms: u64,
57    /// Jitter ratio expressed as a fraction between zero and one.
58    pub jitter_ratio: f64,
59    /// Heartbeat interval in milliseconds.
60    pub heartbeat_interval_ms: u64,
61    /// Stale heartbeat threshold in milliseconds.
62    pub stale_after_ms: u64,
63}
64
65/// Shutdown coordination configuration.
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67pub struct ShutdownConfig {
68    /// Graceful drain timeout in milliseconds.
69    pub graceful_timeout_ms: u64,
70    /// Abort wait timeout in milliseconds.
71    pub abort_wait_ms: u64,
72}
73
74/// Observability configuration.
75#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
76pub struct ObservabilityConfig {
77    /// Event journal capacity.
78    pub event_journal_capacity: usize,
79    /// Whether metrics recording is enabled.
80    pub metrics_enabled: bool,
81    /// Whether command audit recording is enabled.
82    pub audit_enabled: bool,
83}
84
85impl TryFrom<SupervisorConfig> for ConfigState {
86    type Error = crate::error::types::SupervisorError;
87
88    /// Converts a deserialized supervisor config into validated state.
89    fn try_from(config: SupervisorConfig) -> Result<Self, Self::Error> {
90        validate_policy(&config.policy)?;
91        validate_shutdown(&config.shutdown)?;
92        validate_observability(&config.observability)?;
93        Ok(Self {
94            supervisor: config.supervisor,
95            policy: config.policy,
96            shutdown: config.shutdown,
97            observability: config.observability,
98        })
99    }
100}
101
102impl ConfigState {
103    /// Converts validated configuration into a supervisor declaration.
104    ///
105    /// # Arguments
106    ///
107    /// This function has no arguments.
108    ///
109    /// # Returns
110    ///
111    /// Returns a [`crate::spec::supervisor::SupervisorSpec`] derived from the
112    /// validated YAML configuration.
113    ///
114    /// # Examples
115    ///
116    /// ```
117    /// let yaml = r#"
118    /// supervisor:
119    ///   strategy: OneForAll
120    /// policy:
121    ///   child_restart_limit: 10
122    ///   child_restart_window_ms: 60000
123    ///   supervisor_failure_limit: 30
124    ///   supervisor_failure_window_ms: 60000
125    ///   initial_backoff_ms: 10
126    ///   max_backoff_ms: 1000
127    ///   jitter_ratio: 0.0
128    ///   heartbeat_interval_ms: 1000
129    ///   stale_after_ms: 3000
130    /// shutdown:
131    ///   graceful_timeout_ms: 1000
132    ///   abort_wait_ms: 100
133    /// observability:
134    ///   event_journal_capacity: 64
135    ///   metrics_enabled: true
136    ///   audit_enabled: true
137    /// "#;
138    /// let state = rust_supervisor::config::yaml::parse_config_state(yaml).unwrap();
139    /// let spec = state.to_supervisor_spec().unwrap();
140    /// assert_eq!(spec.strategy, rust_supervisor::spec::supervisor::SupervisionStrategy::OneForAll);
141    /// assert_eq!(spec.supervisor_failure_limit, 30);
142    /// ```
143    pub fn to_supervisor_spec(
144        &self,
145    ) -> Result<crate::spec::supervisor::SupervisorSpec, crate::error::types::SupervisorError> {
146        let mut spec = crate::spec::supervisor::SupervisorSpec::root(Vec::new());
147        spec.strategy = self.supervisor.strategy;
148        spec.config_version = self.config_version();
149        spec.supervisor_failure_limit = self.policy.supervisor_failure_limit;
150        spec.control_channel_capacity = self.observability.event_journal_capacity;
151        spec.event_channel_capacity = self.observability.event_journal_capacity;
152        spec.default_backoff_policy = crate::spec::child::BackoffPolicy::new(
153            Duration::from_millis(self.policy.initial_backoff_ms),
154            Duration::from_millis(self.policy.max_backoff_ms),
155            self.policy.jitter_ratio,
156        );
157        spec.default_health_policy = crate::spec::child::HealthPolicy::new(
158            Duration::from_millis(self.policy.heartbeat_interval_ms),
159            Duration::from_millis(self.policy.stale_after_ms),
160        );
161        spec.default_shutdown_policy = crate::spec::child::ShutdownPolicy::new(
162            Duration::from_millis(self.shutdown.graceful_timeout_ms),
163            Duration::from_millis(self.shutdown.abort_wait_ms),
164        );
165        spec.validate()?;
166        Ok(spec)
167    }
168
169    /// Builds a stable configuration version string from configured values.
170    ///
171    /// # Arguments
172    ///
173    /// This function has no arguments.
174    ///
175    /// # Returns
176    ///
177    /// Returns a deterministic version string for diagnostics.
178    fn config_version(&self) -> String {
179        format!(
180            "supervisor-{:?}-policy-{}-{}-shutdown-{}-observe-{}",
181            self.supervisor.strategy,
182            self.policy.child_restart_limit,
183            self.policy.supervisor_failure_limit,
184            self.shutdown.graceful_timeout_ms,
185            self.observability.event_journal_capacity
186        )
187    }
188}
189
190/// Validates policy configuration invariants.
191///
192/// # Arguments
193///
194/// - `policy`: Policy configuration loaded from YAML.
195///
196/// # Returns
197///
198/// Returns `Ok(())` when policy values are usable.
199fn validate_policy(policy: &PolicyConfig) -> Result<(), crate::error::types::SupervisorError> {
200    validate_positive(policy.child_restart_limit, "child_restart_limit")?;
201    validate_positive(policy.supervisor_failure_limit, "supervisor_failure_limit")?;
202    validate_positive(policy.child_restart_window_ms, "child_restart_window_ms")?;
203    validate_positive(
204        policy.supervisor_failure_window_ms,
205        "supervisor_failure_window_ms",
206    )?;
207    validate_positive(policy.initial_backoff_ms, "initial_backoff_ms")?;
208    validate_positive(policy.max_backoff_ms, "max_backoff_ms")?;
209    validate_positive(policy.heartbeat_interval_ms, "heartbeat_interval_ms")?;
210    validate_positive(policy.stale_after_ms, "stale_after_ms")?;
211    if policy.initial_backoff_ms > policy.max_backoff_ms {
212        return Err(crate::error::types::SupervisorError::fatal_config(
213            "initial_backoff_ms must be less than or equal to max_backoff_ms",
214        ));
215    }
216    if !(0.0..=1.0).contains(&policy.jitter_ratio) {
217        return Err(crate::error::types::SupervisorError::fatal_config(
218            "jitter_ratio must be between 0 and 1",
219        ));
220    }
221    Ok(())
222}
223
224/// Validates shutdown configuration invariants.
225///
226/// # Arguments
227///
228/// - `shutdown`: Shutdown configuration loaded from YAML.
229///
230/// # Returns
231///
232/// Returns `Ok(())` when shutdown values are usable.
233fn validate_shutdown(
234    shutdown: &ShutdownConfig,
235) -> Result<(), crate::error::types::SupervisorError> {
236    validate_positive(shutdown.graceful_timeout_ms, "graceful_timeout_ms")?;
237    validate_positive(shutdown.abort_wait_ms, "abort_wait_ms")
238}
239
240/// Validates observability configuration invariants.
241///
242/// # Arguments
243///
244/// - `observability`: Observability configuration loaded from YAML.
245///
246/// # Returns
247///
248/// Returns `Ok(())` when observability values are usable.
249fn validate_observability(
250    observability: &ObservabilityConfig,
251) -> Result<(), crate::error::types::SupervisorError> {
252    validate_positive(
253        observability.event_journal_capacity as u64,
254        "event_journal_capacity",
255    )
256}
257
258/// Validates that a runtime configuration number is positive.
259///
260/// # Arguments
261///
262/// - `value`: Runtime configuration number.
263/// - `name`: Configuration key name.
264///
265/// # Returns
266///
267/// Returns `Ok(())` when the value is positive.
268fn validate_positive(
269    value: impl Into<u64>,
270    name: &str,
271) -> Result<(), crate::error::types::SupervisorError> {
272    if value.into() == 0 {
273        Err(crate::error::types::SupervisorError::fatal_config(format!(
274            "{name} must be greater than zero"
275        )))
276    } else {
277        Ok(())
278    }
279}