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}