use rust_supervisor::error::types::SupervisorError;
use rust_supervisor::id::types::ChildId;
use rust_supervisor::policy::budget::RestartBudgetConfig;
use rust_supervisor::policy::failure_window::FailureWindowConfig;
use rust_supervisor::policy::group::{GroupDependencyEdge, PropagationPolicy};
use rust_supervisor::policy::meltdown::MeltdownPolicy;
use rust_supervisor::policy::task_role_defaults::{SeverityClass, TaskRole};
use rust_supervisor::spec::child::TaskKind;
use rust_supervisor::spec::child::{BackoffPolicy, ChildSpec, HealthPolicy, RestartPolicy};
use rust_supervisor::spec::child_builder::ChildSpecBuilder;
use rust_supervisor::spec::shutdown::{ShutdownBudget, TreeShutdownPolicy};
use rust_supervisor::spec::supervisor::{
BackpressureConfig, BackpressureStrategy, ChildStrategyOverride, DynamicSupervisorPolicy,
EscalationPolicy, GroupConfig, GroupStrategy, RECOMMENDED_CHANNEL_CAPACITY, RestartLimit,
SupervisionStrategy, SupervisorSpec,
};
use rust_supervisor::spec::supervisor_builder::SupervisorSpecBuilder;
use rust_supervisor::task::factory::{TaskResult, service_fn};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
fn service_child(id: &str) -> Result<ChildSpec, SupervisorError> {
let factory = service_fn(|_ctx| async { TaskResult::Succeeded });
ChildSpecBuilder::service(
ChildId::new(id),
id,
TaskKind::AsyncWorker,
Arc::new(factory),
)
.build()
}
fn grouped_service_child(id: &str, group: &str) -> Result<ChildSpec, SupervisorError> {
let factory = service_fn(|_ctx| async { TaskResult::Succeeded });
ChildSpecBuilder::service(
ChildId::new(id),
id,
TaskKind::AsyncWorker,
Arc::new(factory),
)
.group(group)
.build()
}
#[test]
fn root_builder_matches_supervisor_spec_root_defaults() -> Result<(), SupervisorError> {
let builder_spec = SupervisorSpecBuilder::root(Vec::new()).build()?;
let root_spec = SupervisorSpec::root(Vec::new());
assert_eq!(builder_spec.path, root_spec.path);
assert_eq!(builder_spec.strategy, root_spec.strategy);
assert_eq!(builder_spec.children.len(), root_spec.children.len());
assert_eq!(builder_spec.config_version, root_spec.config_version);
assert_eq!(
builder_spec.default_restart_policy,
root_spec.default_restart_policy
);
assert_eq!(
builder_spec.default_backoff_policy,
root_spec.default_backoff_policy
);
assert_eq!(
builder_spec.default_health_policy,
root_spec.default_health_policy
);
assert_eq!(builder_spec.tree_shutdown, root_spec.tree_shutdown);
assert_eq!(
builder_spec.supervisor_failure_limit,
root_spec.supervisor_failure_limit
);
assert_eq!(builder_spec.restart_limit, root_spec.restart_limit);
assert_eq!(builder_spec.escalation_policy, root_spec.escalation_policy);
assert_eq!(builder_spec.group_strategies, root_spec.group_strategies);
assert_eq!(builder_spec.group_configs, root_spec.group_configs);
assert_eq!(
builder_spec.group_dependencies,
root_spec.group_dependencies
);
assert_eq!(builder_spec.severity_defaults, root_spec.severity_defaults);
assert_eq!(
builder_spec.child_strategy_overrides,
root_spec.child_strategy_overrides
);
assert_eq!(
builder_spec.dynamic_supervisor_policy,
root_spec.dynamic_supervisor_policy
);
assert_eq!(
builder_spec.control_channel_capacity,
root_spec.control_channel_capacity
);
assert_eq!(
builder_spec.event_channel_capacity,
root_spec.event_channel_capacity
);
assert_eq!(
builder_spec.backpressure_config,
root_spec.backpressure_config
);
assert_eq!(builder_spec.meltdown_policy, root_spec.meltdown_policy);
assert_eq!(
builder_spec.failure_window_config,
root_spec.failure_window_config
);
assert_eq!(
builder_spec.restart_budget_config,
root_spec.restart_budget_config
);
assert_eq!(
builder_spec.pipeline_journal_capacity,
root_spec.pipeline_journal_capacity
);
assert_eq!(
builder_spec.pipeline_subscriber_capacity,
root_spec.pipeline_subscriber_capacity
);
assert_eq!(
builder_spec.concurrent_restart_limit,
root_spec.concurrent_restart_limit
);
assert_eq!(builder_spec.metrics_enabled, root_spec.metrics_enabled);
assert_eq!(builder_spec.audit_enabled, root_spec.audit_enabled);
Ok(())
}
#[test]
fn child_setter_keeps_recommended_channel_capacities() -> Result<(), SupervisorError> {
let spec = SupervisorSpecBuilder::root(Vec::new())
.child(service_child("api")?)
.build()?;
assert_eq!(spec.children.len(), 1);
assert_eq!(spec.control_channel_capacity, RECOMMENDED_CHANNEL_CAPACITY);
assert_eq!(spec.event_channel_capacity, RECOMMENDED_CHANNEL_CAPACITY);
Ok(())
}
#[test]
fn explicit_channel_capacities_are_preserved_after_child_changes() -> Result<(), SupervisorError> {
let spec = SupervisorSpecBuilder::root(Vec::new())
.control_channel_capacity(32)
.event_channel_capacity(64)
.child(service_child("api")?)
.build()?;
assert_eq!(spec.children.len(), 1);
assert_eq!(spec.control_channel_capacity, 32);
assert_eq!(spec.event_channel_capacity, 64);
Ok(())
}
struct FluentSetterFixture {
child: ChildSpec,
restart_limit: RestartLimit,
spec: SupervisorSpec,
}
fn build_supervisor_with_fluent_setters(
child: ChildSpec,
restart_limit: RestartLimit,
group_config: GroupConfig,
group_strategy: GroupStrategy,
child_override: ChildStrategyOverride,
backpressure_config: BackpressureConfig,
severity_defaults: HashMap<TaskRole, SeverityClass>,
) -> Result<SupervisorSpec, SupervisorError> {
SupervisorSpecBuilder::root(Vec::new())
.path(rust_supervisor::id::types::SupervisorPath::root())
.child(child.clone())
.config_version("builder-demo")
.strategy(SupervisionStrategy::OneForAll)
.default_restart_policy(RestartPolicy::Permanent)
.default_backoff_policy(BackoffPolicy::new(
Duration::from_millis(20),
Duration::from_secs(2),
0.0,
))
.default_health_policy(HealthPolicy::new(
Duration::from_secs(2),
Duration::from_secs(6),
))
.tree_shutdown(TreeShutdownPolicy::new(
ShutdownBudget::new(Duration::from_secs(4), Duration::from_secs(1)),
true,
Duration::from_secs(3),
2,
))
.supervisor_failure_limit(2)
.restart_limit(restart_limit)
.escalation_policy(EscalationPolicy::ShutdownTree)
.group_config(group_config)
.group_strategy(group_strategy)
.group_dependency(GroupDependencyEdge {
from_group: "pipeline".to_owned(),
to_group: "storage".to_owned(),
propagation: PropagationPolicy::EscalateOnly,
})
.severity_defaults(severity_defaults)
.severity_default(TaskRole::Worker, SeverityClass::Standard)
.without_severity_default(TaskRole::Worker)
.child_strategy_override(child_override)
.dynamic_supervisor_policy(DynamicSupervisorPolicy::limited(4))
.control_channel_capacity(16)
.event_channel_capacity(32)
.backpressure_config(backpressure_config)
.meltdown_policy(MeltdownPolicy::new(
2,
Duration::from_secs(8),
4,
Duration::from_secs(20),
8,
Duration::from_secs(40),
Duration::from_secs(80),
))
.failure_window_config(FailureWindowConfig::count_sliding(4, 2))
.restart_budget_config(RestartBudgetConfig::new(Duration::from_secs(20), 2, 1.0))
.pipeline_journal_capacity(64)
.pipeline_subscriber_capacity(8)
.concurrent_restart_limit(2)
.metrics_enabled(false)
.audit_enabled(false)
.build()
}
fn fluent_setter_fixture() -> Result<FluentSetterFixture, SupervisorError> {
let child = grouped_service_child("api", "pipeline")?;
let restart_limit = RestartLimit::new(3, Duration::from_secs(30));
let group_config = GroupConfig::new(
"pipeline",
vec![child.id.clone()],
Some(RestartBudgetConfig::new(Duration::from_secs(30), 3, 1.0)),
);
let mut group_strategy = GroupStrategy::new("pipeline", SupervisionStrategy::OneForAll);
group_strategy.restart_limit = Some(restart_limit);
group_strategy.escalation_policy = Some(EscalationPolicy::EscalateToParent);
let child_override =
ChildStrategyOverride::new(child.id.clone(), SupervisionStrategy::RestForOne);
let backpressure_config = BackpressureConfig {
strategy: BackpressureStrategy::SampleAndAudit,
warn_threshold_pct: 70,
critical_threshold_pct: 90,
window_secs: 10,
audit_channel_capacity: 16,
};
let mut severity_defaults = HashMap::new();
severity_defaults.insert(TaskRole::Service, SeverityClass::Critical);
let spec = build_supervisor_with_fluent_setters(
child.clone(),
restart_limit,
group_config,
group_strategy,
child_override,
backpressure_config,
severity_defaults,
)?;
Ok(FluentSetterFixture {
child,
restart_limit,
spec,
})
}
#[test]
fn builder_setters_apply_expected_fields() -> Result<(), SupervisorError> {
let FluentSetterFixture {
child,
restart_limit,
spec,
} = fluent_setter_fixture()?;
assert_eq!(spec.children.len(), 1);
assert_eq!(spec.children[0].id, child.id);
assert_eq!(spec.config_version, "builder-demo");
assert_eq!(spec.strategy, SupervisionStrategy::OneForAll);
assert_eq!(spec.default_restart_policy, RestartPolicy::Permanent);
assert_eq!(spec.supervisor_failure_limit, 2);
assert_eq!(spec.restart_limit, Some(restart_limit));
assert_eq!(spec.escalation_policy, Some(EscalationPolicy::ShutdownTree));
assert_eq!(spec.group_configs.len(), 1);
assert_eq!(spec.group_strategies.len(), 1);
assert_eq!(spec.group_dependencies.len(), 1);
assert_eq!(
spec.severity_defaults.get(&TaskRole::Service),
Some(&SeverityClass::Critical)
);
assert!(!spec.severity_defaults.contains_key(&TaskRole::Worker));
assert_eq!(spec.child_strategy_overrides.len(), 1);
assert_eq!(spec.dynamic_supervisor_policy.child_limit, Some(4));
assert_eq!(spec.control_channel_capacity, 16);
assert_eq!(spec.event_channel_capacity, 32);
assert_eq!(
spec.backpressure_config.strategy,
BackpressureStrategy::SampleAndAudit
);
assert_eq!(spec.pipeline_journal_capacity, 64);
assert_eq!(spec.pipeline_subscriber_capacity, 8);
assert_eq!(spec.concurrent_restart_limit, 2);
assert!(!spec.metrics_enabled);
assert!(!spec.audit_enabled);
assert_eq!(spec.tree_shutdown.force_kill_margin, Duration::from_secs(3));
assert_eq!(spec.tree_shutdown.max_orphan_threshold, 2);
assert_eq!(
spec.tree_shutdown.budget.graceful_timeout,
Duration::from_secs(4)
);
Ok(())
}
#[test]
fn build_rejects_invalid_supervisor_spec() {
let error = SupervisorSpecBuilder::root(Vec::new())
.config_version(" ")
.build()
.expect_err("empty config version should fail validation");
assert!(error.to_string().contains("config version"));
}