use crate::errors::SelfwareError;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::{mpsc, RwLock};
use tracing::{debug, error, info, warn};
pub trait ComponentFactory: Send + Sync {
fn create(&self) -> Arc<dyn Send + Sync>;
fn stop(&self, component: &Arc<dyn Send + Sync>);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChildState {
Starting,
Running,
Restarting,
Stopped,
Failed,
}
struct ChildRuntime {
state: ChildState,
instance: Option<Arc<dyn Send + Sync>>,
last_started: Option<Instant>,
restart_count: u32,
}
impl std::fmt::Debug for ChildRuntime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ChildRuntime")
.field("state", &self.state)
.field("has_instance", &self.instance.is_some())
.field("last_started", &self.last_started)
.field("restart_count", &self.restart_count)
.finish()
}
}
pub mod circuit_breaker;
pub mod health;
pub mod run_registry;
pub mod run_supervisor;
pub use circuit_breaker::CircuitBreaker;
pub use health::{HealthCheck, HealthMonitor, HealthStatus};
pub use run_registry::{AbortOutcome, RunRecord, RunRegistry};
pub use run_supervisor::{RunId, RunStatus, RunSupervisor};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum BackoffStrategy {
Exponential { base_seconds: u64, max_seconds: u64 },
Fixed { seconds: u64 },
}
impl BackoffStrategy {
pub fn duration(&self, attempt: u32) -> Duration {
match self {
Self::Exponential {
base_seconds,
max_seconds,
} => {
let secs = (*base_seconds).checked_shl(attempt).unwrap_or(*max_seconds);
Duration::from_secs(secs.min(*max_seconds))
}
Self::Fixed { seconds } => Duration::from_secs(*seconds),
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RestartPolicy {
pub max_restarts: u32,
pub max_seconds: u32,
pub backoff_strategy: BackoffStrategy,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum SupervisionStrategy {
OneForOne,
OneForAll,
RestForOne,
}
pub struct Supervisor {
strategy: SupervisionStrategy,
restart_policy: RestartPolicy,
children: Vec<ChildSpec>,
parent_tx: Option<mpsc::Sender<ParentNotification>>,
}
#[derive(Debug, Clone)]
pub enum ParentNotification {
Escalation {
supervisor_id: String,
child_id: String,
error: String,
restart_count: u32,
},
StateChange {
child_id: String,
old_state: ChildState,
new_state: ChildState,
},
}
#[derive(Clone)]
pub struct ChildSpec {
pub id: String,
pub restart_type: RestartType,
pub max_restarts: Option<u32>,
pub factory: Option<Arc<dyn ComponentFactory>>,
}
impl std::fmt::Debug for ChildSpec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ChildSpec")
.field("id", &self.id)
.field("restart_type", &self.restart_type)
.field("max_restarts", &self.max_restarts)
.field("has_factory", &self.factory.is_some())
.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RestartType {
Permanent, Transient, Temporary, }
#[derive(Debug, Clone)]
pub enum ChildEvent {
Crashed {
child_id: String,
error: String,
},
Exited {
child_id: String,
reason: ExitReason,
},
Heartbeat {
child_id: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExitReason {
Normal,
Error,
Killed,
Timeout,
}
#[derive(Debug, Clone)]
pub struct SupervisorHandle {
pub tx: mpsc::Sender<ChildEvent>,
}
pub struct SupervisorBuilder {
strategy: SupervisionStrategy,
restart_policy: RestartPolicy,
children: Vec<ChildSpec>,
parent_tx: Option<mpsc::Sender<ParentNotification>>,
}
impl Supervisor {
pub fn builder() -> SupervisorBuilder {
SupervisorBuilder {
strategy: SupervisionStrategy::OneForOne,
restart_policy: RestartPolicy {
max_restarts: 5,
max_seconds: 60,
backoff_strategy: BackoffStrategy::Exponential {
base_seconds: 1,
max_seconds: 60,
},
},
children: Vec::new(),
parent_tx: None,
}
}
pub async fn start(self) -> Result<SupervisorHandle, SelfwareError> {
let (tx, mut rx) = mpsc::channel(100);
let restart_counts: Arc<RwLock<HashMap<String, Vec<Instant>>>> =
Arc::new(RwLock::new(HashMap::new()));
let child_states: Arc<RwLock<HashMap<String, ChildRuntime>>> =
Arc::new(RwLock::new(HashMap::new()));
{
let mut states = child_states.write().await;
for child in &self.children {
states.insert(
child.id.clone(),
ChildRuntime {
state: ChildState::Stopped,
instance: None,
last_started: None,
restart_count: 0,
},
);
}
}
let supervisor_id = self
.parent_tx
.as_ref()
.map(|_| format!("supervisor-{:p}", &self))
.unwrap_or_default();
tokio::spawn(async move {
info!("Supervision tree started");
for child in &self.children {
if child.restart_type == RestartType::Permanent {
if let Err(e) = self.start_child(&child.id, &child_states).await {
error!(child_id = %child.id, error = %e, "Failed to start child");
}
}
}
while let Some(event) = rx.recv().await {
match event {
ChildEvent::Crashed { child_id, error } => {
error!(child_id = %child_id, error = %error, "Child crashed");
{
let mut states = child_states.write().await;
if let Some(runtime) = states.get_mut(&child_id) {
runtime.state = ChildState::Failed;
}
}
let should_restart = self
.should_restart_child(&child_id, &restart_counts, &error)
.await;
if should_restart {
if self.should_restart(&child_id, &restart_counts).await {
{
let mut counts = restart_counts.write().await;
counts
.entry(child_id.clone())
.or_default()
.push(Instant::now());
}
let backoff =
self.calculate_backoff(&child_id, &restart_counts).await;
warn!(child_id = %child_id, backoff_ms = backoff.as_millis(), "Restarting child");
tokio::time::sleep(backoff).await;
if let Err(e) = self.restart_child(&child_id, &child_states).await {
error!(child_id = %child_id, error = %e, "Failed to restart child, escalating");
self.escalate(
&supervisor_id,
&child_id,
&error,
&restart_counts,
)
.await;
}
} else {
error!(child_id = %child_id, "Max restarts exceeded, escalating");
self.escalate(&supervisor_id, &child_id, &error, &restart_counts)
.await;
}
} else {
info!(child_id = %child_id, "Child will not be restarted based on restart type");
}
}
ChildEvent::Exited { child_id, reason } => {
if reason != ExitReason::Normal {
warn!(child_id = %child_id, reason = ?reason, "Child exited abnormally");
self.handle_abnormal_exit(
&child_id,
reason,
&child_states,
&restart_counts,
)
.await;
} else {
debug!(child_id = %child_id, "Child exited normally");
let mut states = child_states.write().await;
if let Some(runtime) = states.get_mut(&child_id) {
runtime.state = ChildState::Stopped;
runtime.instance = None;
}
}
}
ChildEvent::Heartbeat { child_id } => {
debug!(child_id = %child_id, "Heartbeat received");
}
}
}
for child in &self.children {
if let Err(e) = self.stop_child(&child.id, &child_states).await {
error!(child_id = %child.id, error = %e, "Error stopping child during shutdown");
}
}
info!("Supervision tree stopped");
});
Ok(SupervisorHandle { tx })
}
async fn should_restart_child(
&self,
child_id: &str,
_restart_counts: &Arc<RwLock<HashMap<String, Vec<Instant>>>>,
_error: &str,
) -> bool {
let child_spec = match self.children.iter().find(|c| c.id == child_id) {
Some(spec) => spec,
None => {
warn!(child_id = %child_id, "Unknown child crashed");
return false;
}
};
match child_spec.restart_type {
RestartType::Permanent => true,
RestartType::Transient => {
true
}
RestartType::Temporary => {
info!(child_id = %child_id, "Temporary child crashed, not restarting");
false
}
}
}
async fn start_child(
&self,
child_id: &str,
child_states: &Arc<RwLock<HashMap<String, ChildRuntime>>>,
) -> Result<(), SelfwareError> {
let child_spec = self
.children
.iter()
.find(|c| c.id == child_id)
.ok_or_else(|| SelfwareError::Internal(format!("Child {} not found", child_id)))?;
let factory = child_spec
.factory
.as_ref()
.ok_or_else(|| SelfwareError::Internal(format!("Child {} has no factory", child_id)))?;
{
let mut states = child_states.write().await;
if let Some(runtime) = states.get_mut(child_id) {
runtime.state = ChildState::Starting;
}
}
info!(child_id = %child_id, "Starting child component");
let instance = factory.create();
{
let mut states = child_states.write().await;
if let Some(runtime) = states.get_mut(child_id) {
runtime.state = ChildState::Running;
runtime.instance = Some(instance);
runtime.last_started = Some(Instant::now());
}
}
info!(child_id = %child_id, "Child component started successfully");
Ok(())
}
async fn stop_child(
&self,
child_id: &str,
child_states: &Arc<RwLock<HashMap<String, ChildRuntime>>>,
) -> Result<(), SelfwareError> {
let child_spec = self
.children
.iter()
.find(|c| c.id == child_id)
.ok_or_else(|| SelfwareError::Internal(format!("Child {} not found", child_id)))?;
let mut states = child_states.write().await;
if let Some(runtime) = states.get_mut(child_id) {
if let Some(ref instance) = runtime.instance {
if let Some(ref factory) = child_spec.factory {
factory.stop(instance);
info!(child_id = %child_id, "Child component stopped");
}
}
runtime.state = ChildState::Stopped;
runtime.instance = None;
}
Ok(())
}
async fn should_restart(
&self,
child_id: &str,
restart_counts: &Arc<RwLock<HashMap<String, Vec<Instant>>>>,
) -> bool {
let counts = restart_counts.read().await;
if let Some(times) = counts.get(child_id) {
let window_start =
Instant::now() - Duration::from_secs(self.restart_policy.max_seconds as u64);
let recent_restarts = times.iter().filter(|&&t| t > window_start).count();
recent_restarts < self.restart_policy.max_restarts as usize
} else {
true
}
}
async fn calculate_backoff(
&self,
child_id: &str,
_restart_counts: &Arc<RwLock<HashMap<String, Vec<Instant>>>>,
) -> Duration {
let counts = _restart_counts.read().await;
let attempt = counts.get(child_id).map(|v| v.len() as u32).unwrap_or(0);
self.restart_policy.backoff_strategy.duration(attempt)
}
async fn restart_child(
&self,
child_id: &str,
child_states: &Arc<RwLock<HashMap<String, ChildRuntime>>>,
) -> Result<(), SelfwareError> {
info!(child_id = %child_id, "Restarting child component");
{
let mut states = child_states.write().await;
if let Some(runtime) = states.get_mut(child_id) {
let old_state = runtime.state;
runtime.state = ChildState::Restarting;
runtime.restart_count += 1;
info!(
child_id = %child_id,
restart_count = runtime.restart_count,
old_state = ?old_state,
"Child state changed to restarting"
);
}
}
self.stop_child(child_id, child_states).await?;
info!(child_id = %child_id, "Failed component stopped");
{
let mut states = child_states.write().await;
if let Some(runtime) = states.get_mut(child_id) {
runtime.instance = None;
}
}
info!(child_id = %child_id, "Child state cleared");
self.start_child(child_id, child_states).await?;
info!(child_id = %child_id, "Fresh child instance started");
{
let mut states = child_states.write().await;
if let Some(runtime) = states.get_mut(child_id) {
runtime.state = ChildState::Running;
info!(
child_id = %child_id,
state = ?runtime.state,
restart_count = runtime.restart_count,
"Health tracking updated after successful restart"
);
}
}
Ok(())
}
async fn handle_abnormal_exit(
&self,
child_id: &str,
reason: ExitReason,
child_states: &Arc<RwLock<HashMap<String, ChildRuntime>>>,
_restart_counts: &Arc<RwLock<HashMap<String, Vec<Instant>>>>,
) {
warn!(child_id = %child_id, reason = ?reason, "Handling abnormal exit");
let child_spec = match self.children.iter().find(|c| c.id == child_id) {
Some(spec) => spec,
None => {
warn!(child_id = %child_id, "Unknown child exited abnormally");
return;
}
};
let should_restart = match child_spec.restart_type {
RestartType::Permanent => true,
RestartType::Transient => reason != ExitReason::Normal,
RestartType::Temporary => false,
};
if !should_restart {
info!(child_id = %child_id, "Child will not be restarted based on restart type");
return;
}
match self.strategy {
SupervisionStrategy::OneForAll => {
info!("OneForAll strategy: restarting all children");
for child in &self.children {
if let Err(e) = self.restart_child(&child.id, child_states).await {
error!(child_id = %child.id, error = %e, "Failed to restart child in OneForAll");
}
}
}
SupervisionStrategy::RestForOne => {
info!("RestForOne strategy: restarting failed child and subsequent children");
let mut restart = false;
for child in &self.children {
if child.id == child_id {
restart = true;
}
if restart {
if let Err(e) = self.restart_child(&child.id, child_states).await {
error!(child_id = %child.id, error = %e, "Failed to restart child in RestForOne");
}
}
}
}
SupervisionStrategy::OneForOne => {
info!("OneForOne strategy: restarting failed child only");
if let Err(e) = self.restart_child(child_id, child_states).await {
error!(child_id = %child_id, error = %e, "Failed to restart child in OneForOne");
}
}
}
}
async fn escalate(
&self,
supervisor_id: &str,
child_id: &str,
error: &str,
_restart_counts: &Arc<RwLock<HashMap<String, Vec<Instant>>>>,
) {
let restart_count = {
let counts = _restart_counts.read().await;
counts.get(child_id).map(|v| v.len() as u32).unwrap_or(0)
};
error!(
child_id = %child_id,
error = %error,
restart_count = restart_count,
"Escalating failure to parent supervisor"
);
if let Some(ref parent_tx) = self.parent_tx {
let notification = ParentNotification::Escalation {
supervisor_id: supervisor_id.to_string(),
child_id: child_id.to_string(),
error: error.to_string(),
restart_count,
};
match parent_tx.try_send(notification) {
Ok(()) => {
info!(
child_id = %child_id,
"Successfully notified parent supervisor of escalation"
);
}
Err(mpsc::error::TrySendError::Full(_)) => {
error!(
child_id = %child_id,
"Parent supervisor channel full, dropping escalation notification"
);
}
Err(mpsc::error::TrySendError::Closed(_)) => {
error!(
child_id = %child_id,
"Parent supervisor channel closed, cannot escalate"
);
}
}
} else {
error!(
child_id = %child_id,
"No parent supervisor available. This is a critical failure at the top level."
);
}
}
}
impl SupervisorBuilder {
pub fn with_strategy(mut self, strategy: SupervisionStrategy) -> Self {
self.strategy = strategy;
self
}
pub fn with_restart_policy(mut self, policy: RestartPolicy) -> Self {
self.restart_policy = policy;
self
}
pub fn with_parent(mut self, parent_tx: mpsc::Sender<ParentNotification>) -> Self {
self.parent_tx = Some(parent_tx);
self
}
pub fn add_child(mut self, id: impl Into<String>, factory: Arc<dyn ComponentFactory>) -> Self {
self.children.push(ChildSpec {
id: id.into(),
restart_type: RestartType::Permanent,
max_restarts: None,
factory: Some(factory),
});
self
}
pub fn build(self) -> Supervisor {
Supervisor {
strategy: self.strategy,
restart_policy: self.restart_policy,
children: self.children,
parent_tx: self.parent_tx,
}
}
}
#[cfg(test)]
#[path = "../../tests/unit/supervision/mod_test.rs"]
mod tests;