use crate::id::types::ChildId;
use crate::spec::child::{BackoffPolicy, RestartPolicy};
use crate::spec::supervisor::{EscalationPolicy, RestartLimit};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum TaskRole {
Service,
Worker,
Job,
Sidecar,
Supervisor,
}
impl TaskRole {
pub const fn as_str(self) -> &'static str {
match self {
Self::Service => "service",
Self::Worker => "worker",
Self::Job => "job",
Self::Sidecar => "sidecar",
Self::Supervisor => "supervisor",
}
}
}
impl Display for TaskRole {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct SidecarConfig {
pub primary_child_id: ChildId,
#[serde(default)]
pub linked_lifecycle: bool,
}
impl SidecarConfig {
pub fn new(primary_child_id: ChildId, linked_lifecycle: bool) -> Self {
Self {
primary_child_id,
linked_lifecycle,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum OnSuccessAction {
Restart,
Stop,
NoOp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum OnFailureAction {
RestartWithBackoff,
RestartPermanent,
StopAndEscalate,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum OnManualStopAction {
StopForever,
StopUntilExplicitRestart,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum OnTimeoutAction {
RestartWithBackoff,
StopAndEscalate,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum OnBudgetExhaustedAction {
StopAndEscalate,
Quarantine,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct RoleDefaultPolicy {
pub on_success_exit: OnSuccessAction,
pub on_failure_exit: OnFailureAction,
pub on_manual_stop: OnManualStopAction,
pub on_timeout: OnTimeoutAction,
pub on_budget_exhausted: OnBudgetExhaustedAction,
pub default_restart_limit: Option<RestartLimit>,
pub default_escalation_policy: Option<EscalationPolicy>,
pub default_backoff_policy: Option<BackoffPolicy>,
#[serde(default = "default_success_exit_codes")]
pub success_exit_codes: Vec<i32>,
}
struct RoleDefaultPolicyDifferences {
on_success_exit: OnSuccessAction,
on_timeout: OnTimeoutAction,
max_restarts: u32,
}
impl From<RoleDefaultPolicyDifferences> for RoleDefaultPolicy {
fn from(differences: RoleDefaultPolicyDifferences) -> Self {
Self {
on_success_exit: differences.on_success_exit,
on_failure_exit: OnFailureAction::RestartWithBackoff,
on_manual_stop: OnManualStopAction::StopForever,
on_timeout: differences.on_timeout,
on_budget_exhausted: OnBudgetExhaustedAction::StopAndEscalate,
default_restart_limit: Some(bounded_restart_limit(differences.max_restarts)),
default_escalation_policy: Some(EscalationPolicy::EscalateToParent),
default_backoff_policy: Some(default_backoff_policy()),
success_exit_codes: default_success_exit_codes(),
}
}
}
impl RoleDefaultPolicy {
pub fn for_role(role: TaskRole) -> Self {
match role {
TaskRole::Service => service_default(),
TaskRole::Worker => worker_default(),
TaskRole::Job => job_default(),
TaskRole::Sidecar => sidecar_default(),
TaskRole::Supervisor => supervisor_default(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum PolicySource {
RoleDefault,
UserOverride,
FallbackDefault,
}
impl Display for PolicySource {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
let label = match self {
Self::RoleDefault => "role_default",
Self::UserOverride => "user_override",
Self::FallbackDefault => "fallback_default",
};
formatter.write_str(label)
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
)]
pub enum SeverityClass {
Optional,
Standard,
Critical,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct EffectivePolicy {
pub task_role: TaskRole,
pub policy_pack: RoleDefaultPolicy,
pub source: PolicySource,
pub used_fallback: bool,
pub overridden_fields: Vec<String>,
pub severity: SeverityClass,
pub group_name: Option<String>,
}
impl EffectivePolicy {
pub fn merge(role: Option<TaskRole>, overridden_fields: Vec<String>) -> Self {
let used_fallback = role.is_none();
let task_role = role.unwrap_or(TaskRole::Worker);
let source = if used_fallback {
PolicySource::FallbackDefault
} else if overridden_fields.is_empty() {
PolicySource::RoleDefault
} else {
PolicySource::UserOverride
};
let severity = Self::default_severity(task_role);
Self {
task_role,
policy_pack: RoleDefaultPolicy::for_role(task_role),
source,
used_fallback,
overridden_fields,
severity,
group_name: None,
}
}
fn default_severity(role: TaskRole) -> SeverityClass {
match role {
TaskRole::Service => SeverityClass::Critical,
TaskRole::Supervisor => SeverityClass::Critical,
TaskRole::Worker => SeverityClass::Standard,
TaskRole::Job => SeverityClass::Optional,
TaskRole::Sidecar => SeverityClass::Standard,
}
}
pub fn for_child(child: &crate::spec::child::ChildSpec) -> Self {
let mut overridden = Vec::new();
if child.restart_policy != RestartPolicy::Transient {
overridden.push("restart_policy".to_string());
}
let effective_policy = Self::merge(child.task_role, overridden);
if child.task_role.is_none() {
tracing::warn!(
child_id = %child.id,
task_role = %effective_policy.task_role,
used_fallback_default = effective_policy.used_fallback,
effective_policy_source = %effective_policy.source,
"task role missing, falling back to worker default"
);
}
effective_policy
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RoleSemanticConflict {
pub child_id: ChildId,
pub task_role: TaskRole,
pub conflicting_field: String,
pub user_value: String,
pub expected_semantic: String,
pub reason: String,
}
pub fn semantic_conflicts_for_child(
child: &crate::spec::child::ChildSpec,
) -> Vec<RoleSemanticConflict> {
let mut conflicts = Vec::new();
if child.task_role == Some(TaskRole::Job) && child.restart_policy == RestartPolicy::Permanent {
conflicts.push(RoleSemanticConflict {
child_id: child.id.clone(),
task_role: TaskRole::Job,
conflicting_field: "restart_policy".to_string(),
user_value: "permanent".to_string(),
expected_semantic: "job success should stop".to_string(),
reason: "Job role must not silently use permanent restart semantics".to_string(),
});
}
conflicts
}
fn default_success_exit_codes() -> Vec<i32> {
vec![0]
}
fn bounded_restart_limit(max_restarts: u32) -> RestartLimit {
RestartLimit::new(max_restarts, Duration::from_secs(60))
}
fn default_backoff_policy() -> BackoffPolicy {
BackoffPolicy::new(Duration::from_millis(50), Duration::from_secs(5), 0.2)
}
fn service_default() -> RoleDefaultPolicy {
RoleDefaultPolicyDifferences {
on_success_exit: OnSuccessAction::Restart,
on_timeout: OnTimeoutAction::RestartWithBackoff,
max_restarts: 10,
}
.into()
}
fn worker_default() -> RoleDefaultPolicy {
RoleDefaultPolicyDifferences {
on_success_exit: OnSuccessAction::Stop,
on_timeout: OnTimeoutAction::RestartWithBackoff,
max_restarts: 3,
}
.into()
}
fn job_default() -> RoleDefaultPolicy {
RoleDefaultPolicyDifferences {
on_success_exit: OnSuccessAction::Stop,
on_timeout: OnTimeoutAction::StopAndEscalate,
max_restarts: 1,
}
.into()
}
fn sidecar_default() -> RoleDefaultPolicy {
RoleDefaultPolicyDifferences {
on_success_exit: OnSuccessAction::Restart,
on_timeout: OnTimeoutAction::RestartWithBackoff,
max_restarts: 5,
}
.into()
}
fn supervisor_default() -> RoleDefaultPolicy {
RoleDefaultPolicyDifferences {
on_success_exit: OnSuccessAction::Restart,
on_timeout: OnTimeoutAction::RestartWithBackoff,
max_restarts: 3,
}
.into()
}