use crate::error::types::SupervisorError;
use crate::id::types::ChildId;
use crate::policy::task_role_defaults::{SeverityClass, SidecarConfig, TaskRole};
use crate::readiness::signal::ReadinessPolicy;
use crate::task::factory::TaskFactory;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt::{Debug, Formatter};
use std::sync::Arc;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum TaskKind {
AsyncWorker,
BlockingWorker,
Supervisor,
}
impl Default for TaskKind {
fn default() -> Self {
Self::AsyncWorker
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Criticality {
Critical,
Optional,
}
impl Default for Criticality {
fn default() -> Self {
Self::Optional
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum RestartPolicy {
Permanent,
Transient,
Temporary,
}
impl Default for RestartPolicy {
fn default() -> Self {
Self::Permanent
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
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, Serialize, Deserialize, JsonSchema)]
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, Eq, Serialize, Deserialize, JsonSchema)]
pub struct HealthCheckConfig {
pub check_interval_secs: u64,
pub timeout_secs: u64,
pub max_retries: u32,
}
impl Default for HealthCheckConfig {
fn default() -> Self {
Self {
check_interval_secs: 10,
timeout_secs: 5,
max_retries: 3,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ReadinessConfig {
pub check_interval_secs: u64,
pub timeout_secs: u64,
}
impl Default for ReadinessConfig {
fn default() -> Self {
Self {
check_interval_secs: 5,
timeout_secs: 3,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ResourceLimits {
pub max_memory_mb: Option<u64>,
pub max_cpu_percent: Option<u8>,
pub max_file_descriptors: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct CommandPermissions {
pub allow_shutdown: bool,
pub allow_restart: bool,
pub allowed_signals: Vec<String>,
}
impl Default for CommandPermissions {
fn default() -> Self {
Self {
allow_shutdown: false,
allow_restart: false,
allowed_signals: vec!["SIGTERM".to_string()],
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct EnvVar {
pub name: String,
pub value: Option<String>,
pub secret_ref: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct SecretRef {
pub name: String,
pub key: String,
pub required: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
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, Serialize, Deserialize, JsonSchema)]
pub struct ChildSpec {
pub id: ChildId,
pub name: String,
pub kind: TaskKind,
#[serde(skip)]
#[schemars(skip)]
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,
#[serde(default)]
pub task_role: Option<TaskRole>,
#[serde(default)]
pub sidecar_config: Option<SidecarConfig>,
#[serde(default)]
pub severity: Option<SeverityClass>,
#[serde(default)]
pub group: Option<String>,
#[serde(default)]
pub health_check: Option<HealthCheckConfig>,
#[serde(default)]
pub readiness: Option<ReadinessConfig>,
#[serde(default)]
pub resource_limits: Option<ResourceLimits>,
#[serde(default)]
pub command_permissions: CommandPermissions,
#[serde(default)]
pub environment: Vec<EnvVar>,
#[serde(default)]
pub secrets: Vec<SecretRef>,
}
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)
.field("task_role", &self.task_role)
.field("sidecar_config", &self.sidecar_config)
.field("severity", &self.severity)
.field("group", &self.group)
.field("health_check", &self.health_check)
.field("readiness", &self.readiness)
.field("resource_limits", &self.resource_limits)
.field("command_permissions", &self.command_permissions)
.field("environment", &self.environment)
.field("secrets", &self.secrets)
.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,
task_role: Some(TaskRole::Worker),
sidecar_config: None,
severity: None,
group: None,
health_check: None,
readiness: None,
resource_limits: None,
command_permissions: CommandPermissions::default(),
environment: Vec::new(),
secrets: Vec::new(),
}
}
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())?;
validate_sidecar_local(self)
}
}
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(()),
}
}
fn validate_sidecar_local(child: &ChildSpec) -> Result<(), SupervisorError> {
match (child.task_role, child.sidecar_config.as_ref()) {
(Some(TaskRole::Sidecar), None) => Err(SupervisorError::fatal_config(
"sidecar task_role requires sidecar_config",
)),
(role, Some(_)) if role != Some(TaskRole::Sidecar) => Err(SupervisorError::fatal_config(
"sidecar_config requires sidecar task_role",
)),
_ => Ok(()),
}
}