use crate::error::types::SupervisorError;
use crate::id::types::ChildId;
use crate::readiness::signal::ReadinessPolicy;
use crate::task::factory::TaskFactory;
use std::fmt::{Debug, Formatter};
use std::sync::Arc;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TaskKind {
AsyncWorker,
BlockingWorker,
Supervisor,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Criticality {
Critical,
Optional,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RestartPolicy {
Permanent,
Transient,
Temporary,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ShutdownPolicy {
pub graceful_timeout: Duration,
pub abort_wait: Duration,
}
impl ShutdownPolicy {
pub fn new(graceful_timeout: Duration, abort_wait: Duration) -> Self {
Self {
graceful_timeout,
abort_wait,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HealthPolicy {
pub heartbeat_interval: Duration,
pub stale_after: Duration,
}
impl HealthPolicy {
pub fn new(heartbeat_interval: Duration, stale_after: Duration) -> Self {
Self {
heartbeat_interval,
stale_after,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BackoffPolicy {
pub initial_delay: Duration,
pub max_delay: Duration,
pub jitter_ratio: f64,
}
impl BackoffPolicy {
pub fn new(initial_delay: Duration, max_delay: Duration, jitter_ratio: f64) -> Self {
Self {
initial_delay,
max_delay,
jitter_ratio,
}
}
}
#[derive(Clone)]
pub struct ChildSpec {
pub id: ChildId,
pub name: String,
pub kind: TaskKind,
pub factory: Option<Arc<dyn TaskFactory>>,
pub restart_policy: RestartPolicy,
pub shutdown_policy: ShutdownPolicy,
pub health_policy: HealthPolicy,
pub readiness_policy: ReadinessPolicy,
pub backoff_policy: BackoffPolicy,
pub dependencies: Vec<ChildId>,
pub tags: Vec<String>,
pub criticality: Criticality,
}
impl Debug for ChildSpec {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("ChildSpec")
.field("id", &self.id)
.field("name", &self.name)
.field("kind", &self.kind)
.field("restart_policy", &self.restart_policy)
.field("shutdown_policy", &self.shutdown_policy)
.field("health_policy", &self.health_policy)
.field("readiness_policy", &self.readiness_policy)
.field("backoff_policy", &self.backoff_policy)
.field("dependencies", &self.dependencies)
.field("tags", &self.tags)
.field("criticality", &self.criticality)
.finish()
}
}
impl ChildSpec {
pub fn worker(
id: ChildId,
name: impl Into<String>,
kind: TaskKind,
factory: Arc<dyn TaskFactory>,
) -> Self {
Self {
id,
name: name.into(),
kind,
factory: Some(factory),
restart_policy: RestartPolicy::Transient,
shutdown_policy: ShutdownPolicy::new(Duration::from_secs(5), Duration::from_secs(1)),
health_policy: HealthPolicy::new(Duration::from_secs(1), Duration::from_secs(3)),
readiness_policy: ReadinessPolicy::Immediate,
backoff_policy: BackoffPolicy::new(
Duration::from_millis(10),
Duration::from_secs(1),
0.0,
),
dependencies: Vec::new(),
tags: Vec::new(),
criticality: Criticality::Critical,
}
}
pub fn validate(&self) -> Result<(), SupervisorError> {
validate_non_empty(&self.id.value, "child id")?;
validate_non_empty(&self.name, "child name")?;
validate_tags(&self.tags)?;
validate_backoff(self.backoff_policy)?;
validate_factory(self.kind, self.factory.is_some())
}
}
fn validate_non_empty(value: &str, label: &str) -> Result<(), SupervisorError> {
if value.trim().is_empty() {
Err(SupervisorError::fatal_config(format!(
"{label} must not be empty"
)))
} else {
Ok(())
}
}
fn validate_tags(tags: &[String]) -> Result<(), SupervisorError> {
for tag in tags {
validate_non_empty(tag, "child tag")?;
}
Ok(())
}
fn validate_backoff(policy: BackoffPolicy) -> Result<(), SupervisorError> {
if policy.initial_delay > policy.max_delay {
return Err(SupervisorError::fatal_config(
"initial backoff must not exceed max backoff",
));
}
if !(0.0..=1.0).contains(&policy.jitter_ratio) {
return Err(SupervisorError::fatal_config(
"jitter ratio must be between zero and one",
));
}
Ok(())
}
fn validate_factory(kind: TaskKind, has_factory: bool) -> Result<(), SupervisorError> {
match (kind, has_factory) {
(TaskKind::Supervisor, true) => Err(SupervisorError::fatal_config(
"supervisor child must not own a task factory",
)),
(TaskKind::AsyncWorker | TaskKind::BlockingWorker, false) => Err(
SupervisorError::fatal_config("worker child requires a task factory"),
),
_ => Ok(()),
}
}