Skip to main content

rust_supervisor/config/
policy.rs

1//! YAML-friendly policy configuration models.
2//!
3//! This module owns configuration input structs for lower-level supervision
4//! policy objects whose runtime form uses `Duration`, `ChildId`, or other
5//! strongly typed values.
6
7use confique::Config;
8use schemars::{JsonSchema, Schema, SchemaGenerator};
9use serde::{Deserialize, Serialize};
10use std::borrow::Cow;
11use std::time::Duration;
12
13use crate::id::types::ChildId;
14use crate::policy::budget as runtime_budget;
15use crate::policy::failure_window as runtime_failure_window;
16use crate::policy::group::{GroupDependencyEdge, PropagationPolicy};
17use crate::policy::meltdown::MeltdownPolicy;
18use crate::policy::task_role_defaults::{SeverityClass, TaskRole};
19use crate::spec::supervisor::{
20    ChildStrategyOverride, DynamicSupervisorPolicy, EscalationPolicy,
21    GroupConfig as RuntimeGroupConfig, GroupStrategy, RestartLimit, SupervisionStrategy,
22};
23
24/// Restart budget configuration loaded from YAML.
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Config, JsonSchema)]
26pub struct RestartBudgetConfig {
27    /// Sliding window duration in seconds.
28    #[config(default = 60)]
29    #[serde(default = "default_restart_budget_window_secs")]
30    pub window_secs: u64,
31    /// Maximum burst failures allowed within the window.
32    #[config(default = 10)]
33    #[serde(default = "default_restart_budget_max_burst")]
34    pub max_burst: u32,
35    /// Token recovery rate per second.
36    #[config(default = 0.5)]
37    #[serde(default = "default_restart_budget_recovery_rate")]
38    pub recovery_rate_per_sec: f64,
39}
40
41impl RestartBudgetConfig {
42    /// Converts this YAML-friendly config into the runtime restart budget.
43    ///
44    /// # Arguments
45    ///
46    /// This function has no arguments.
47    ///
48    /// # Returns
49    ///
50    /// Returns a [`runtime_budget::RestartBudgetConfig`] value.
51    pub fn to_runtime(&self) -> runtime_budget::RestartBudgetConfig {
52        runtime_budget::RestartBudgetConfig::new(
53            Duration::from_secs(self.window_secs),
54            self.max_burst,
55            self.recovery_rate_per_sec,
56        )
57    }
58}
59
60impl Default for RestartBudgetConfig {
61    /// Returns the default restart budget configuration.
62    fn default() -> Self {
63        Self {
64            window_secs: default_restart_budget_window_secs(),
65            max_burst: default_restart_budget_max_burst(),
66            recovery_rate_per_sec: default_restart_budget_recovery_rate(),
67        }
68    }
69}
70
71/// Failure window mode loaded from YAML.
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
73#[serde(rename_all = "snake_case")]
74pub enum FailureWindowMode {
75    /// Count failures that occur inside a time window.
76    TimeSliding,
77    /// Keep the most recent failure samples by count.
78    CountSliding,
79}
80
81impl Default for FailureWindowMode {
82    /// Returns the default time-sliding mode.
83    fn default() -> Self {
84        Self::TimeSliding
85    }
86}
87
88/// Failure window configuration loaded from YAML.
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Config, JsonSchema)]
90pub struct FailureWindowConfig {
91    /// Window mode selection.
92    #[config(default = "time_sliding")]
93    #[serde(default)]
94    pub mode: FailureWindowMode,
95    /// Time window width in seconds for `time_sliding` mode.
96    #[config(default = 60)]
97    #[serde(default = "default_failure_window_secs")]
98    pub window_secs: u64,
99    /// Maximum retained failure count for `count_sliding` mode.
100    #[config(default = 5)]
101    #[serde(default = "default_failure_window_max_count")]
102    pub max_count: usize,
103    /// Failure threshold at which the window is considered exhausted.
104    #[config(default = 5)]
105    #[serde(default = "default_failure_window_threshold")]
106    pub threshold: usize,
107}
108
109impl FailureWindowConfig {
110    /// Converts this YAML-friendly config into the runtime failure window.
111    ///
112    /// # Arguments
113    ///
114    /// This function has no arguments.
115    ///
116    /// # Returns
117    ///
118    /// Returns a [`runtime_failure_window::FailureWindowConfig`] value.
119    pub fn to_runtime(&self) -> runtime_failure_window::FailureWindowConfig {
120        match self.mode {
121            FailureWindowMode::TimeSliding => {
122                runtime_failure_window::FailureWindowConfig::time_sliding(
123                    self.window_secs,
124                    self.threshold,
125                )
126            }
127            FailureWindowMode::CountSliding => {
128                runtime_failure_window::FailureWindowConfig::count_sliding(
129                    self.max_count,
130                    self.threshold,
131                )
132            }
133        }
134    }
135}
136
137impl Default for FailureWindowConfig {
138    /// Returns the default failure window configuration.
139    fn default() -> Self {
140        Self {
141            mode: FailureWindowMode::default(),
142            window_secs: default_failure_window_secs(),
143            max_count: default_failure_window_max_count(),
144            threshold: default_failure_window_threshold(),
145        }
146    }
147}
148
149/// Meltdown fuse configuration loaded from YAML.
150#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Config, JsonSchema)]
151pub struct MeltdownConfig {
152    /// Maximum restarts allowed for one child inside the child window.
153    #[config(default = 3)]
154    #[serde(default = "default_meltdown_child_max_restarts")]
155    pub child_max_restarts: u32,
156    /// Window used to count child restarts, in seconds.
157    #[config(default = 10)]
158    #[serde(default = "default_meltdown_child_window_secs")]
159    pub child_window_secs: u64,
160    /// Maximum failures allowed for one group inside the group window.
161    #[config(default = 5)]
162    #[serde(default = "default_meltdown_group_max_failures")]
163    pub group_max_failures: u32,
164    /// Window used to count group failures, in seconds.
165    #[config(default = 30)]
166    #[serde(default = "default_meltdown_group_window_secs")]
167    pub group_window_secs: u64,
168    /// Maximum failures allowed for the supervisor inside the supervisor window.
169    #[config(default = 10)]
170    #[serde(default = "default_meltdown_supervisor_max_failures")]
171    pub supervisor_max_failures: u32,
172    /// Window used to count supervisor failures, in seconds.
173    #[config(default = 60)]
174    #[serde(default = "default_meltdown_supervisor_window_secs")]
175    pub supervisor_window_secs: u64,
176    /// Stable duration after which recorded counters may be cleared, in seconds.
177    #[config(default = 120)]
178    #[serde(default = "default_meltdown_reset_after_secs")]
179    pub reset_after_secs: u64,
180}
181
182impl MeltdownConfig {
183    /// Converts this YAML-friendly config into the runtime meltdown policy.
184    ///
185    /// # Arguments
186    ///
187    /// This function has no arguments.
188    ///
189    /// # Returns
190    ///
191    /// Returns a [`MeltdownPolicy`] value.
192    pub fn to_runtime(&self) -> MeltdownPolicy {
193        MeltdownPolicy::new(
194            self.child_max_restarts,
195            Duration::from_secs(self.child_window_secs),
196            self.group_max_failures,
197            Duration::from_secs(self.group_window_secs),
198            self.supervisor_max_failures,
199            Duration::from_secs(self.supervisor_window_secs),
200            Duration::from_secs(self.reset_after_secs),
201        )
202    }
203}
204
205impl Default for MeltdownConfig {
206    /// Returns the default meltdown fuse configuration.
207    fn default() -> Self {
208        Self {
209            child_max_restarts: default_meltdown_child_max_restarts(),
210            child_window_secs: default_meltdown_child_window_secs(),
211            group_max_failures: default_meltdown_group_max_failures(),
212            group_window_secs: default_meltdown_group_window_secs(),
213            supervisor_max_failures: default_meltdown_supervisor_max_failures(),
214            supervisor_window_secs: default_meltdown_supervisor_window_secs(),
215            reset_after_secs: default_meltdown_reset_after_secs(),
216        }
217    }
218}
219
220/// Supervision pipeline capacities loaded from YAML.
221#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Config, JsonSchema)]
222pub struct SupervisionPipelineConfig {
223    /// Event journal capacity used by the supervision pipeline.
224    #[config(default = 100)]
225    #[serde(default = "default_pipeline_journal_capacity")]
226    pub journal_capacity: usize,
227    /// Subscriber queue capacity used by the supervision pipeline.
228    #[config(default = 10)]
229    #[serde(default = "default_pipeline_subscriber_capacity")]
230    pub subscriber_capacity: usize,
231    /// Maximum concurrent restarts allowed for one supervisor instance.
232    #[config(default = 5)]
233    #[serde(default = "default_concurrent_restart_limit")]
234    pub concurrent_restart_limit: u32,
235}
236
237impl Default for SupervisionPipelineConfig {
238    /// Returns the default supervision pipeline capacities.
239    fn default() -> Self {
240        Self {
241            journal_capacity: default_pipeline_journal_capacity(),
242            subscriber_capacity: default_pipeline_subscriber_capacity(),
243            concurrent_restart_limit: default_concurrent_restart_limit(),
244        }
245    }
246}
247
248/// Dynamic child acceptance policy loaded from YAML.
249#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Config, JsonSchema)]
250pub struct DynamicSupervisorConfig {
251    /// Whether runtime child additions are accepted.
252    #[config(default = true)]
253    #[serde(default = "default_true")]
254    pub enabled: bool,
255    /// Optional maximum number of declared and dynamic children.
256    #[schemars(!default)]
257    #[serde(default)]
258    pub child_limit: Option<usize>,
259}
260
261impl DynamicSupervisorConfig {
262    /// Converts this YAML-friendly config into the runtime dynamic policy.
263    ///
264    /// # Arguments
265    ///
266    /// This function has no arguments.
267    ///
268    /// # Returns
269    ///
270    /// Returns a [`DynamicSupervisorPolicy`] value.
271    pub fn to_runtime(&self) -> DynamicSupervisorPolicy {
272        DynamicSupervisorPolicy {
273            enabled: self.enabled,
274            child_limit: self.child_limit,
275        }
276    }
277}
278
279impl Default for DynamicSupervisorConfig {
280    /// Returns the default dynamic supervisor policy.
281    fn default() -> Self {
282        Self {
283            enabled: true,
284            child_limit: None,
285        }
286    }
287}
288
289/// Restart limit configuration loaded from YAML.
290#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Config, JsonSchema)]
291pub struct RestartLimitConfig {
292    /// Maximum restart count inside the configured window.
293    pub max_restarts: u32,
294    /// Window used to count restarts, in milliseconds.
295    pub window_ms: u64,
296}
297
298impl RestartLimitConfig {
299    /// Converts this YAML-friendly config into a runtime restart limit.
300    ///
301    /// # Arguments
302    ///
303    /// This function has no arguments.
304    ///
305    /// # Returns
306    ///
307    /// Returns a [`RestartLimit`] value.
308    pub fn to_runtime(&self) -> RestartLimit {
309        RestartLimit::new(self.max_restarts, Duration::from_millis(self.window_ms))
310    }
311}
312
313/// Split-friendly nested section for group policy declarations.
314///
315/// Single-file configs use `groups: [...]`. Split `groups.yaml` files contain
316/// only the group entry sequence for this section.
317#[derive(Debug, Clone, PartialEq, Config)]
318pub struct GroupsConfigSection {
319    /// Group entries loaded from the `groups` configuration section.
320    #[config(default = [])]
321    pub items: Vec<GroupConfig>,
322}
323
324impl Default for GroupsConfigSection {
325    /// Returns an empty group configuration section.
326    fn default() -> Self {
327        Self { items: Vec::new() }
328    }
329}
330
331impl JsonSchema for GroupsConfigSection {
332    /// Returns the schema name used for split group sections.
333    fn schema_name() -> Cow<'static, str> {
334        Cow::Borrowed("GroupsConfigSection")
335    }
336
337    /// Returns the transparent array schema for group declarations.
338    fn json_schema(generator: &mut SchemaGenerator) -> Schema {
339        Vec::<GroupConfig>::json_schema(generator)
340    }
341}
342
343impl Serialize for GroupsConfigSection {
344    /// Serializes group section entries as a transparent array.
345    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
346    where
347        S: confique::serde::Serializer,
348    {
349        self.items.serialize(serializer)
350    }
351}
352
353impl<'de> Deserialize<'de> for GroupsConfigSection {
354    /// Deserializes group section entries from a transparent array.
355    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
356    where
357        D: confique::serde::Deserializer<'de>,
358    {
359        Ok(Self {
360            items: Vec::<GroupConfig>::deserialize(deserializer)?,
361        })
362    }
363}
364
365impl GroupsConfigSection {
366    /// Returns group entries as a slice.
367    pub fn as_slice(&self) -> &[GroupConfig] {
368        &self.items
369    }
370
371    /// Returns the number of group entries.
372    pub fn len(&self) -> usize {
373        self.items.len()
374    }
375
376    /// Returns whether this section contains no group entries.
377    pub fn is_empty(&self) -> bool {
378        self.items.is_empty()
379    }
380}
381
382impl From<GroupsConfigSection> for Vec<GroupConfig> {
383    /// Converts a group section into its transparent entry vector.
384    fn from(section: GroupsConfigSection) -> Self {
385        section.items
386    }
387}
388
389/// Group-level configuration loaded from YAML.
390#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Config, JsonSchema)]
391pub struct GroupConfig {
392    /// Low-cardinality group name shared by member children.
393    pub name: String,
394    /// Optional group-specific restart budget override.
395    #[schemars(!default)]
396    #[serde(default)]
397    pub budget: Option<RestartBudgetConfig>,
398}
399
400impl GroupConfig {
401    /// Converts this YAML-friendly config into a runtime group config.
402    ///
403    /// # Arguments
404    ///
405    /// - `members`: Child identifiers derived from `children[].group` at load time.
406    ///
407    /// # Returns
408    ///
409    /// Returns a [`RuntimeGroupConfig`] value.
410    pub fn to_runtime(&self, members: &[ChildId]) -> RuntimeGroupConfig {
411        RuntimeGroupConfig::new(
412            self.name.clone(),
413            members.to_vec(),
414            self.budget.as_ref().map(RestartBudgetConfig::to_runtime),
415        )
416    }
417}
418
419/// Group strategy override loaded from YAML.
420#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Config, JsonSchema)]
421pub struct GroupStrategyConfig {
422    /// Group name that owns the strategy override.
423    pub group: String,
424    /// Restart strategy used when a member child fails.
425    pub strategy: SupervisionStrategy,
426    /// Optional group-level restart limit.
427    #[schemars(!default)]
428    #[serde(default)]
429    pub restart_limit: Option<RestartLimitConfig>,
430    /// Optional escalation policy for this group.
431    #[schemars(!default)]
432    #[serde(default)]
433    pub escalation_policy: Option<EscalationPolicy>,
434}
435
436impl GroupStrategyConfig {
437    /// Converts this YAML-friendly config into a runtime group strategy.
438    ///
439    /// # Arguments
440    ///
441    /// This function has no arguments.
442    ///
443    /// # Returns
444    ///
445    /// Returns a [`GroupStrategy`] value.
446    pub fn to_runtime(&self) -> GroupStrategy {
447        let mut strategy = GroupStrategy::new(self.group.clone(), self.strategy);
448        strategy.restart_limit = self
449            .restart_limit
450            .as_ref()
451            .map(RestartLimitConfig::to_runtime);
452        strategy.escalation_policy = self.escalation_policy;
453        strategy
454    }
455}
456
457/// Child strategy override loaded from YAML.
458#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Config, JsonSchema)]
459pub struct ChildStrategyOverrideConfig {
460    /// Child name that owns the strategy override.
461    pub child_id: String,
462    /// Restart strategy used for this child.
463    pub strategy: SupervisionStrategy,
464    /// Optional child-level restart limit.
465    #[schemars(!default)]
466    #[serde(default)]
467    pub restart_limit: Option<RestartLimitConfig>,
468    /// Optional escalation policy for this child.
469    #[schemars(!default)]
470    #[serde(default)]
471    pub escalation_policy: Option<EscalationPolicy>,
472}
473
474impl ChildStrategyOverrideConfig {
475    /// Converts this YAML-friendly config into a runtime child strategy override.
476    ///
477    /// # Arguments
478    ///
479    /// This function has no arguments.
480    ///
481    /// # Returns
482    ///
483    /// Returns a [`ChildStrategyOverride`] value.
484    pub fn to_runtime(&self) -> ChildStrategyOverride {
485        let mut override_config =
486            ChildStrategyOverride::new(ChildId::new(&self.child_id), self.strategy);
487        override_config.restart_limit = self
488            .restart_limit
489            .as_ref()
490            .map(RestartLimitConfig::to_runtime);
491        override_config.escalation_policy = self.escalation_policy;
492        override_config
493    }
494}
495
496/// Group dependency edge loaded from YAML.
497#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Config, JsonSchema)]
498pub struct GroupDependencyConfig {
499    /// Group that depends on another group.
500    pub from_group: String,
501    /// Group that is depended on.
502    pub to_group: String,
503    /// Failure propagation policy.
504    pub propagation: PropagationPolicy,
505}
506
507impl GroupDependencyConfig {
508    /// Converts this YAML-friendly config into a runtime dependency edge.
509    ///
510    /// # Arguments
511    ///
512    /// This function has no arguments.
513    ///
514    /// # Returns
515    ///
516    /// Returns a [`GroupDependencyEdge`] value.
517    pub fn to_runtime(&self) -> GroupDependencyEdge {
518        GroupDependencyEdge {
519            from_group: self.from_group.clone(),
520            to_group: self.to_group.clone(),
521            propagation: self.propagation,
522        }
523    }
524}
525
526/// Severity default loaded from YAML.
527#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Config, JsonSchema)]
528pub struct SeverityDefaultConfig {
529    /// Task role that receives this default severity.
530    pub task_role: TaskRole,
531    /// Severity assigned to the task role.
532    pub severity: SeverityClass,
533}
534
535/// Returns the default restart budget window in seconds.
536fn default_restart_budget_window_secs() -> u64 {
537    60
538}
539
540/// Returns the default restart budget burst.
541fn default_restart_budget_max_burst() -> u32 {
542    10
543}
544
545/// Returns the default restart budget recovery rate per second.
546fn default_restart_budget_recovery_rate() -> f64 {
547    0.5
548}
549
550/// Returns the default failure window width in seconds.
551fn default_failure_window_secs() -> u64 {
552    60
553}
554
555/// Returns the default retained failure count.
556fn default_failure_window_max_count() -> usize {
557    5
558}
559
560/// Returns the default failure threshold.
561fn default_failure_window_threshold() -> usize {
562    5
563}
564
565/// Returns the default child meltdown limit.
566fn default_meltdown_child_max_restarts() -> u32 {
567    3
568}
569
570/// Returns the default child meltdown window in seconds.
571fn default_meltdown_child_window_secs() -> u64 {
572    10
573}
574
575/// Returns the default group meltdown limit.
576fn default_meltdown_group_max_failures() -> u32 {
577    5
578}
579
580/// Returns the default group meltdown window in seconds.
581fn default_meltdown_group_window_secs() -> u64 {
582    30
583}
584
585/// Returns the default supervisor meltdown limit.
586fn default_meltdown_supervisor_max_failures() -> u32 {
587    10
588}
589
590/// Returns the default supervisor meltdown window in seconds.
591fn default_meltdown_supervisor_window_secs() -> u64 {
592    60
593}
594
595/// Returns the default stable reset window in seconds.
596fn default_meltdown_reset_after_secs() -> u64 {
597    120
598}
599
600/// Returns the default supervision pipeline journal capacity.
601fn default_pipeline_journal_capacity() -> usize {
602    100
603}
604
605/// Returns the default supervision pipeline subscriber capacity.
606fn default_pipeline_subscriber_capacity() -> usize {
607    10
608}
609
610/// Returns the default concurrent restart limit.
611fn default_concurrent_restart_limit() -> u32 {
612    5
613}
614
615/// Serde default helper: returns true.
616fn default_true() -> bool {
617    true
618}