Skip to main content

rust_supervisor/spec/
child.rs

1//! Child declaration model.
2//!
3//! This module owns declarative child specifications and validates local child
4//! invariants before the runtime registers or starts work.
5
6use crate::error::types::SupervisorError;
7use crate::id::types::ChildId;
8use crate::readiness::signal::ReadinessPolicy;
9use crate::task::factory::TaskFactory;
10use std::fmt::{Debug, Formatter};
11use std::sync::Arc;
12use std::time::Duration;
13
14/// Kind of task represented by a child declaration.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum TaskKind {
17    /// Asynchronous worker that can be cancelled through its context.
18    AsyncWorker,
19    /// Blocking worker with explicit shutdown and escalation boundaries.
20    BlockingWorker,
21    /// Nested supervisor node.
22    Supervisor,
23}
24
25/// Importance of a child to its parent supervisor.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Criticality {
28    /// The child is required for the supervisor to remain healthy.
29    Critical,
30    /// The child can fail without forcing parent shutdown.
31    Optional,
32}
33
34/// Restart behavior attached to a child.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum RestartPolicy {
37    /// Restart regardless of the exit result.
38    Permanent,
39    /// Restart only when the task failed.
40    Transient,
41    /// Do not restart after any exit.
42    Temporary,
43}
44
45/// Shutdown behavior attached to a child.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub struct ShutdownPolicy {
48    /// Graceful stop budget for cooperative shutdown.
49    pub graceful_timeout: Duration,
50    /// Wait budget after an abort request.
51    pub abort_wait: Duration,
52}
53
54impl ShutdownPolicy {
55    /// Creates a shutdown policy.
56    ///
57    /// # Arguments
58    ///
59    /// - `graceful_timeout`: Cooperative shutdown budget.
60    /// - `abort_wait`: Wait budget after abort escalation.
61    ///
62    /// # Returns
63    ///
64    /// Returns a [`ShutdownPolicy`] value.
65    ///
66    /// # Examples
67    ///
68    /// ```
69    /// let policy = rust_supervisor::spec::child::ShutdownPolicy::new(
70    ///     std::time::Duration::from_secs(1),
71    ///     std::time::Duration::from_millis(100),
72    /// );
73    /// assert_eq!(policy.graceful_timeout.as_secs(), 1);
74    /// ```
75    pub fn new(graceful_timeout: Duration, abort_wait: Duration) -> Self {
76        Self {
77            graceful_timeout,
78            abort_wait,
79        }
80    }
81}
82
83/// Health behavior attached to a child.
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct HealthPolicy {
86    /// Expected heartbeat interval.
87    pub heartbeat_interval: Duration,
88    /// Maximum age for the last heartbeat before the child is stale.
89    pub stale_after: Duration,
90}
91
92impl HealthPolicy {
93    /// Creates a health policy.
94    ///
95    /// # Arguments
96    ///
97    /// - `heartbeat_interval`: Expected heartbeat interval.
98    /// - `stale_after`: Maximum heartbeat age.
99    ///
100    /// # Returns
101    ///
102    /// Returns a [`HealthPolicy`] value.
103    pub fn new(heartbeat_interval: Duration, stale_after: Duration) -> Self {
104        Self {
105            heartbeat_interval,
106            stale_after,
107        }
108    }
109}
110
111/// Backoff behavior attached to a child.
112#[derive(Debug, Clone, Copy, PartialEq)]
113pub struct BackoffPolicy {
114    /// Initial delay before the first restart.
115    pub initial_delay: Duration,
116    /// Maximum restart delay.
117    pub max_delay: Duration,
118    /// Jitter ratio between zero and one.
119    pub jitter_ratio: f64,
120}
121
122impl BackoffPolicy {
123    /// Creates a backoff policy.
124    ///
125    /// # Arguments
126    ///
127    /// - `initial_delay`: Initial restart delay.
128    /// - `max_delay`: Maximum restart delay.
129    /// - `jitter_ratio`: Jitter ratio between zero and one.
130    ///
131    /// # Returns
132    ///
133    /// Returns a [`BackoffPolicy`] value.
134    pub fn new(initial_delay: Duration, max_delay: Duration, jitter_ratio: f64) -> Self {
135        Self {
136            initial_delay,
137            max_delay,
138            jitter_ratio,
139        }
140    }
141}
142
143/// Declarative specification for a child task or nested supervisor.
144#[derive(Clone)]
145pub struct ChildSpec {
146    /// Stable child identifier.
147    pub id: ChildId,
148    /// Human-readable child name.
149    pub name: String,
150    /// Child task kind.
151    pub kind: TaskKind,
152    /// Optional factory for worker children.
153    pub factory: Option<Arc<dyn TaskFactory>>,
154    /// Restart policy for this child.
155    pub restart_policy: RestartPolicy,
156    /// Shutdown policy for this child.
157    pub shutdown_policy: ShutdownPolicy,
158    /// Health policy for this child.
159    pub health_policy: HealthPolicy,
160    /// Readiness policy for this child.
161    pub readiness_policy: ReadinessPolicy,
162    /// Backoff policy for this child.
163    pub backoff_policy: BackoffPolicy,
164    /// Child identifiers that must become ready before this child starts.
165    pub dependencies: Vec<ChildId>,
166    /// Low-cardinality tags used for grouping and diagnostics.
167    pub tags: Vec<String>,
168    /// Criticality used by parent policy decisions.
169    pub criticality: Criticality,
170}
171
172impl Debug for ChildSpec {
173    /// Formats the child specification without printing the task factory.
174    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
175        formatter
176            .debug_struct("ChildSpec")
177            .field("id", &self.id)
178            .field("name", &self.name)
179            .field("kind", &self.kind)
180            .field("restart_policy", &self.restart_policy)
181            .field("shutdown_policy", &self.shutdown_policy)
182            .field("health_policy", &self.health_policy)
183            .field("readiness_policy", &self.readiness_policy)
184            .field("backoff_policy", &self.backoff_policy)
185            .field("dependencies", &self.dependencies)
186            .field("tags", &self.tags)
187            .field("criticality", &self.criticality)
188            .finish()
189    }
190}
191
192impl ChildSpec {
193    /// Creates a worker child specification.
194    ///
195    /// # Arguments
196    ///
197    /// - `id`: Stable child identifier.
198    /// - `name`: Human-readable child name.
199    /// - `kind`: Worker task kind.
200    /// - `factory`: Task factory used to build each attempt.
201    ///
202    /// # Returns
203    ///
204    /// Returns a [`ChildSpec`] with conservative policy values.
205    ///
206    /// # Examples
207    ///
208    /// ```
209    /// let factory = rust_supervisor::task::factory::service_fn(|_ctx| async {
210    ///     rust_supervisor::task::factory::TaskResult::Succeeded
211    /// });
212    /// let spec = rust_supervisor::spec::child::ChildSpec::worker(
213    ///     rust_supervisor::id::types::ChildId::new("worker"),
214    ///     "worker",
215    ///     rust_supervisor::spec::child::TaskKind::AsyncWorker,
216    ///     std::sync::Arc::new(factory),
217    /// );
218    /// assert_eq!(spec.name, "worker");
219    /// ```
220    pub fn worker(
221        id: ChildId,
222        name: impl Into<String>,
223        kind: TaskKind,
224        factory: Arc<dyn TaskFactory>,
225    ) -> Self {
226        Self {
227            id,
228            name: name.into(),
229            kind,
230            factory: Some(factory),
231            restart_policy: RestartPolicy::Transient,
232            shutdown_policy: ShutdownPolicy::new(Duration::from_secs(5), Duration::from_secs(1)),
233            health_policy: HealthPolicy::new(Duration::from_secs(1), Duration::from_secs(3)),
234            readiness_policy: ReadinessPolicy::Immediate,
235            backoff_policy: BackoffPolicy::new(
236                Duration::from_millis(10),
237                Duration::from_secs(1),
238                0.0,
239            ),
240            dependencies: Vec::new(),
241            tags: Vec::new(),
242            criticality: Criticality::Critical,
243        }
244    }
245
246    /// Validates local child specification invariants.
247    ///
248    /// # Arguments
249    ///
250    /// This function has no arguments.
251    ///
252    /// # Returns
253    ///
254    /// Returns `Ok(())` when the child can be registered.
255    pub fn validate(&self) -> Result<(), SupervisorError> {
256        validate_non_empty(&self.id.value, "child id")?;
257        validate_non_empty(&self.name, "child name")?;
258        validate_tags(&self.tags)?;
259        validate_backoff(self.backoff_policy)?;
260        validate_factory(self.kind, self.factory.is_some())
261    }
262}
263
264/// Validates a non-empty string invariant.
265///
266/// # Arguments
267///
268/// - `value`: String value being validated.
269/// - `label`: Field label used in the error message.
270///
271/// # Returns
272///
273/// Returns `Ok(())` when the string is not empty.
274fn validate_non_empty(value: &str, label: &str) -> Result<(), SupervisorError> {
275    if value.trim().is_empty() {
276        Err(SupervisorError::fatal_config(format!(
277            "{label} must not be empty"
278        )))
279    } else {
280        Ok(())
281    }
282}
283
284/// Validates tag invariants.
285///
286/// # Arguments
287///
288/// - `tags`: Tags attached to the child.
289///
290/// # Returns
291///
292/// Returns `Ok(())` when every tag is non-empty.
293fn validate_tags(tags: &[String]) -> Result<(), SupervisorError> {
294    for tag in tags {
295        validate_non_empty(tag, "child tag")?;
296    }
297    Ok(())
298}
299
300/// Validates backoff invariants.
301///
302/// # Arguments
303///
304/// - `policy`: Backoff policy attached to the child.
305///
306/// # Returns
307///
308/// Returns `Ok(())` when delay and jitter values are valid.
309fn validate_backoff(policy: BackoffPolicy) -> Result<(), SupervisorError> {
310    if policy.initial_delay > policy.max_delay {
311        return Err(SupervisorError::fatal_config(
312            "initial backoff must not exceed max backoff",
313        ));
314    }
315    if !(0.0..=1.0).contains(&policy.jitter_ratio) {
316        return Err(SupervisorError::fatal_config(
317            "jitter ratio must be between zero and one",
318        ));
319    }
320    Ok(())
321}
322
323/// Validates factory presence for the child kind.
324///
325/// # Arguments
326///
327/// - `kind`: Child task kind.
328/// - `has_factory`: Whether a factory was supplied.
329///
330/// # Returns
331///
332/// Returns `Ok(())` when factory presence matches the task kind.
333fn validate_factory(kind: TaskKind, has_factory: bool) -> Result<(), SupervisorError> {
334    match (kind, has_factory) {
335        (TaskKind::Supervisor, true) => Err(SupervisorError::fatal_config(
336            "supervisor child must not own a task factory",
337        )),
338        (TaskKind::AsyncWorker | TaskKind::BlockingWorker, false) => Err(
339            SupervisorError::fatal_config("worker child requires a task factory"),
340        ),
341        _ => Ok(()),
342    }
343}