#![allow(dead_code)]
use crate::DaemonContext;
use crate::disk_pressure::{
DiskPressurePolicyConfig, DiskSlotPolicy, PressureAssessment, evaluate_pressure_policy,
};
use crate::health::probe_worker_capabilities;
use rch_common::{
CircuitBreakerConfig, CircuitState, CircuitStats, WorkerCapabilities, WorkerConfig, WorkerId,
WorkerStatus,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicI64, AtomicU32, AtomicU64, AtomicUsize, Ordering};
use std::time::Duration;
use tokio::sync::RwLock;
use tracing::debug;
const CAPABILITIES_REFRESH_PROBE_TIMEOUT: Duration = Duration::from_secs(25);
const CAPABILITIES_REFRESH_WORKER_BUDGET: Duration = Duration::from_secs(35);
fn duration_millis_i64(duration: Duration) -> i64 {
i64::try_from(duration.as_millis()).unwrap_or(i64::MAX)
}
fn duration_secs_i64(duration: Duration) -> i64 {
i64::try_from(duration.as_secs()).unwrap_or(i64::MAX)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AdminIntent {
#[default]
Active,
Draining,
Drained,
Disabled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EligibilityState {
#[default]
Healthy,
Degraded,
Unreachable,
TemporaryBypass,
RecoveredPendingCanary,
}
pub use rch_common::BypassFailureClass;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IllegalLifecycleTransition {
pub from: EligibilityState,
pub to: EligibilityState,
pub reason: &'static str,
}
impl std::fmt::Display for IllegalLifecycleTransition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"illegal worker lifecycle transition {:?} -> {:?}: {}",
self.from, self.to, self.reason
)
}
}
impl std::error::Error for IllegalLifecycleTransition {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct WorkerLifecycle {
pub admin: AdminIntent,
pub eligibility: EligibilityState,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bypass_cause: Option<BypassFailureClass>,
}
impl WorkerLifecycle {
pub fn new() -> Self {
Self {
admin: AdminIntent::Active,
eligibility: EligibilityState::Healthy,
bypass_cause: None,
}
}
pub fn from_worker_status(status: WorkerStatus) -> Self {
match status {
WorkerStatus::Healthy => Self::new(),
WorkerStatus::Degraded => Self {
admin: AdminIntent::Active,
eligibility: EligibilityState::Degraded,
bypass_cause: None,
},
WorkerStatus::Unreachable => Self {
admin: AdminIntent::Active,
eligibility: EligibilityState::Unreachable,
bypass_cause: None,
},
WorkerStatus::Draining => Self {
admin: AdminIntent::Draining,
eligibility: EligibilityState::Healthy,
bypass_cause: None,
},
WorkerStatus::Drained => Self {
admin: AdminIntent::Drained,
eligibility: EligibilityState::Healthy,
bypass_cause: None,
},
WorkerStatus::Disabled => Self {
admin: AdminIntent::Disabled,
eligibility: EligibilityState::Healthy,
bypass_cause: None,
},
}
}
pub fn legacy_status(&self) -> WorkerStatus {
match self.admin {
AdminIntent::Disabled => WorkerStatus::Disabled,
AdminIntent::Draining => WorkerStatus::Draining,
AdminIntent::Drained => WorkerStatus::Drained,
AdminIntent::Active => match self.eligibility {
EligibilityState::Healthy => WorkerStatus::Healthy,
EligibilityState::Degraded => WorkerStatus::Degraded,
EligibilityState::Unreachable
| EligibilityState::TemporaryBypass
| EligibilityState::RecoveredPendingCanary => WorkerStatus::Unreachable,
},
}
}
pub fn is_schedulable(&self) -> bool {
self.admin == AdminIntent::Active
&& matches!(
self.eligibility,
EligibilityState::Healthy | EligibilityState::Degraded
)
}
pub fn is_canary_pending(&self) -> bool {
self.admin == AdminIntent::Active
&& self.eligibility == EligibilityState::RecoveredPendingCanary
}
pub fn set_admin(&mut self, intent: AdminIntent) {
self.admin = intent;
}
pub fn observe_health(&mut self, health: EligibilityState) -> bool {
let is_plain_health = matches!(
health,
EligibilityState::Healthy | EligibilityState::Degraded | EligibilityState::Unreachable
);
let currently_quarantined = matches!(
self.eligibility,
EligibilityState::TemporaryBypass | EligibilityState::RecoveredPendingCanary
);
if !is_plain_health || currently_quarantined {
return false;
}
self.eligibility = health;
true
}
pub fn enter_bypass(&mut self, cause: BypassFailureClass) {
self.eligibility = EligibilityState::TemporaryBypass;
self.bypass_cause = Some(cause);
}
pub fn recover_to_canary(&mut self) -> Result<(), IllegalLifecycleTransition> {
if self.eligibility != EligibilityState::TemporaryBypass {
return Err(IllegalLifecycleTransition {
from: self.eligibility,
to: EligibilityState::RecoveredPendingCanary,
reason: "recovery to canary is only legal from temporary_bypass",
});
}
self.eligibility = EligibilityState::RecoveredPendingCanary;
self.bypass_cause = None;
Ok(())
}
pub fn promote_from_canary(&mut self) -> Result<(), IllegalLifecycleTransition> {
if self.eligibility != EligibilityState::RecoveredPendingCanary {
return Err(IllegalLifecycleTransition {
from: self.eligibility,
to: EligibilityState::Healthy,
reason: "promotion is only legal from recovered_pending_canary",
});
}
self.eligibility = EligibilityState::Healthy;
self.bypass_cause = None;
Ok(())
}
}
fn health_status_to_eligibility(status: WorkerStatus) -> Option<EligibilityState> {
match status {
WorkerStatus::Healthy => Some(EligibilityState::Healthy),
WorkerStatus::Degraded => Some(EligibilityState::Degraded),
WorkerStatus::Unreachable => Some(EligibilityState::Unreachable),
WorkerStatus::Draining | WorkerStatus::Drained | WorkerStatus::Disabled => None,
}
}
fn lifecycle_accepts_new_builds(lifecycle: WorkerLifecycle) -> bool {
!matches!(
lifecycle.admin,
AdminIntent::Draining | AdminIntent::Drained | AdminIntent::Disabled
) && !matches!(
lifecycle.eligibility,
EligibilityState::TemporaryBypass | EligibilityState::RecoveredPendingCanary
)
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
enum DrainCompletionAction {
#[default]
StayDrained,
RemoveFromPool,
Disable {
reason: Option<String>,
},
}
#[derive(Debug)]
pub struct WorkerState {
pub config: RwLock<WorkerConfig>,
lifecycle: RwLock<WorkerLifecycle>,
used_slots: Arc<AtomicU32>,
pub speed_score: AtomicU64,
last_latency_ms: AtomicU64,
pub cached_projects: RwLock<Vec<String>>,
circuit: RwLock<CircuitStats>,
last_error_msg: RwLock<Option<String>>,
capabilities: RwLock<WorkerCapabilities>,
capability_probe: tokio::sync::Mutex<()>,
toolchain_preflight: RwLock<HashMap<String, ToolchainPreflightStatus>>,
pressure_assessment: RwLock<PressureAssessment>,
disk_slot_policy: DiskSlotPolicy,
disabled_reason: RwLock<Option<String>>,
drain_completion: RwLock<DrainCompletionAction>,
disabled_at: AtomicI64,
}
impl WorkerState {
pub fn new(config: WorkerConfig) -> Self {
Self::with_disk_slot_policy(
config,
DiskSlotPolicy::from(&rch_common::SelectionConfig::default()),
)
}
fn with_disk_slot_policy(config: WorkerConfig, disk_slot_policy: DiskSlotPolicy) -> Self {
Self {
config: RwLock::new(config),
lifecycle: RwLock::new(WorkerLifecycle::new()),
used_slots: Arc::new(AtomicU32::new(0)),
speed_score: AtomicU64::new(50.0_f64.to_bits()), last_latency_ms: AtomicU64::new(0),
cached_projects: RwLock::new(Vec::new()),
circuit: RwLock::new(CircuitStats::new()),
last_error_msg: RwLock::new(None),
capabilities: RwLock::new(WorkerCapabilities::new()),
capability_probe: tokio::sync::Mutex::new(()),
toolchain_preflight: RwLock::new(HashMap::new()),
pressure_assessment: RwLock::new(PressureAssessment::default()),
disk_slot_policy,
disabled_reason: RwLock::new(None),
drain_completion: RwLock::new(DrainCompletionAction::default()),
disabled_at: AtomicI64::new(0),
}
}
pub async fn update_config(&self, new_config: WorkerConfig) {
{
let mut config = self.config.write().await;
*config = new_config;
}
let cancelled_pending_removal = {
let mut completion = self.drain_completion.write().await;
let should_cancel = *completion == DrainCompletionAction::RemoveFromPool;
if should_cancel {
*completion = DrainCompletionAction::StayDrained;
}
should_cancel
};
if cancelled_pending_removal {
let mut lifecycle = self.lifecycle.write().await;
if matches!(
lifecycle.admin,
AdminIntent::Draining | AdminIntent::Drained
) {
lifecycle.set_admin(AdminIntent::Active);
}
}
}
pub async fn status(&self) -> WorkerStatus {
self.lifecycle.read().await.legacy_status()
}
pub async fn lifecycle(&self) -> WorkerLifecycle {
*self.lifecycle.read().await
}
pub async fn eligibility(&self) -> EligibilityState {
self.lifecycle.read().await.eligibility
}
pub async fn set_status(&self, status: WorkerStatus) {
*self.lifecycle.write().await = WorkerLifecycle::from_worker_status(status);
}
pub async fn apply_health_status(&self, health_status: WorkerStatus) -> WorkerStatus {
let mut lifecycle = self.lifecycle.write().await;
if let Some(eligibility) = health_status_to_eligibility(health_status) {
lifecycle.observe_health(eligibility);
}
lifecycle.legacy_status()
}
pub async fn enter_bypass(&self, class: BypassFailureClass) {
self.lifecycle.write().await.enter_bypass(class);
}
pub async fn recover_to_canary(&self) -> Result<(), IllegalLifecycleTransition> {
self.lifecycle.write().await.recover_to_canary()
}
pub async fn promote_from_canary(&self) -> Result<(), IllegalLifecycleTransition> {
self.lifecycle.write().await.promote_from_canary()
}
pub async fn is_canary_pending(&self) -> bool {
self.lifecycle.read().await.is_canary_pending()
}
pub async fn effective_total_slots(&self) -> u32 {
let config = self.config.read().await;
let pressure = self.pressure_assessment.read().await;
self.disk_slot_policy
.effective_slots(config.total_slots, &pressure)
}
pub async fn available_slots(&self) -> u32 {
let total = self.effective_total_slots().await;
let used = self.used_slots.load(Ordering::Relaxed);
total.saturating_sub(used)
}
pub async fn disk_headroom(&self) -> f64 {
let assessment = self.pressure_assessment.read().await;
self.disk_slot_policy.headroom(&assessment)
}
pub async fn reserve_slots(&self, count: u32) -> bool {
if count == 0 {
return false;
}
let mut current = self.used_slots.load(Ordering::Relaxed);
loop {
if !lifecycle_accepts_new_builds(*self.lifecycle.read().await) {
return false;
}
let config = self.config.read().await;
let pressure = self.pressure_assessment.read().await;
let total_slots = self
.disk_slot_policy
.effective_slots(config.total_slots, &pressure);
let Some(next) = current.checked_add(count) else {
return false;
};
if next > total_slots {
return false;
}
let reserved = self.used_slots.compare_exchange(
current,
next,
Ordering::SeqCst,
Ordering::Relaxed,
);
drop(pressure);
drop(config);
match reserved {
Ok(_) => {
if lifecycle_accepts_new_builds(*self.lifecycle.read().await)
&& self.used_slots() <= self.effective_total_slots().await
{
return true;
}
self.release_slots(count).await;
return false;
}
Err(actual) => current = actual,
}
}
}
pub async fn release_slots(&self, count: u32) {
let mut current = self.used_slots.load(Ordering::Relaxed);
loop {
let new_val = current.saturating_sub(count);
if count > current {
let id = self.config.read().await.id.clone();
tracing::warn!(
"Worker {}: attempted to release {} slots but only {} in use",
id,
count,
current
);
}
match self.used_slots.compare_exchange(
current,
new_val,
Ordering::SeqCst,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(actual) => current = actual,
}
}
self.check_drain_complete().await;
}
pub async fn has_cached_project(&self, project: &str) -> bool {
self.cached_projects
.read()
.await
.iter()
.any(|p| p == project)
}
pub async fn add_cached_project(&self, project: String) {
let mut cache = self.cached_projects.write().await;
if !cache.contains(&project) {
if cache.len() >= 100 {
cache.remove(0);
}
cache.push(project);
}
}
pub fn set_speed_score(&self, score: f64) {
self.speed_score.store(score.to_bits(), Ordering::Relaxed);
}
pub fn get_speed_score(&self) -> f64 {
f64::from_bits(self.speed_score.load(Ordering::Relaxed))
}
pub fn set_last_latency_ms(&self, latency_ms: Option<u64>) {
self.last_latency_ms
.store(latency_ms.unwrap_or(0), Ordering::Relaxed);
}
pub fn last_latency_ms(&self) -> Option<u64> {
let v = self.last_latency_ms.load(Ordering::Relaxed);
if v == 0 { None } else { Some(v) }
}
pub async fn circuit_state(&self) -> Option<CircuitState> {
Some(self.circuit.read().await.state())
}
pub async fn circuit_stats(&self) -> CircuitStats {
self.circuit.read().await.clone()
}
pub async fn record_success(&self) {
let mut circuit = self.circuit.write().await;
circuit.record_success();
}
pub async fn record_health_check(
&self,
healthy: bool,
transient: bool,
config: &CircuitBreakerConfig,
) -> (CircuitState, CircuitState) {
let mut circuit = self.circuit.write().await;
let previous = circuit.state();
let new = circuit.apply_health_outcome(healthy, transient, config);
(previous, new)
}
pub async fn record_command_outcome(
&self,
success: bool,
config: &CircuitBreakerConfig,
) -> (CircuitState, CircuitState) {
let mut circuit = self.circuit.write().await;
let previous = circuit.state();
let new = circuit.apply_command_outcome(success, config);
(previous, new)
}
pub async fn record_failure(&self, error_msg: Option<String>) {
let mut circuit = self.circuit.write().await;
circuit.record_failure();
if let Some(msg) = error_msg {
*self.last_error_msg.write().await = Some(msg);
}
}
pub async fn should_open_circuit(&self, config: &CircuitBreakerConfig) -> bool {
self.circuit.read().await.should_open(config)
}
pub async fn should_half_open(&self, config: &CircuitBreakerConfig) -> bool {
self.circuit.read().await.should_half_open(config)
}
pub async fn should_close_circuit(&self, config: &CircuitBreakerConfig) -> bool {
self.circuit.read().await.should_close(config)
}
pub async fn can_probe(&self, config: &CircuitBreakerConfig) -> bool {
self.circuit.read().await.can_probe(config)
}
pub async fn start_probe(&self, config: &CircuitBreakerConfig) -> bool {
self.circuit.write().await.start_probe(config)
}
pub async fn open_circuit(&self) {
self.circuit.write().await.open();
}
pub async fn half_open_circuit(&self) {
self.circuit.write().await.half_open();
}
pub async fn close_circuit(&self) {
self.circuit.write().await.close();
*self.last_error_msg.write().await = None;
}
pub async fn last_error(&self) -> Option<String> {
self.last_error_msg.read().await.clone()
}
pub async fn set_error(&self, msg: String) {
*self.last_error_msg.write().await = Some(msg);
}
pub fn try_capability_probe(&self) -> Option<tokio::sync::MutexGuard<'_, ()>> {
self.capability_probe.try_lock().ok()
}
pub async fn set_capabilities(&self, capabilities: WorkerCapabilities) {
let pressure_config = DiskPressurePolicyConfig::default();
let pressure = evaluate_pressure_policy(&capabilities, None, &pressure_config);
*self.capabilities.write().await = capabilities;
let now_ms = current_unix_ms();
let mut current = self.pressure_assessment.write().await;
if pressure.state != crate::disk_pressure::PressureState::Critical
&& refresh_preserved_pressure_age(&mut current, &pressure_config, now_ms)
{
current.disk_free_gb = pressure.disk_free_gb;
current.disk_total_gb = pressure.disk_total_gb;
current.disk_free_ratio = pressure.disk_free_ratio;
current.evaluated_at_unix_ms = now_ms;
return;
}
*current = pressure;
}
pub async fn capabilities(&self) -> WorkerCapabilities {
self.capabilities.read().await.clone()
}
pub async fn record_toolchain_preflight(
&self,
toolchain: String,
usable: bool,
reason: Option<String>,
) {
self.toolchain_preflight.write().await.insert(
toolchain,
ToolchainPreflightStatus {
usable,
reason,
checked_at_unix_ms: current_unix_ms(),
},
);
}
pub async fn toolchain_preflight_status(
&self,
toolchain: &str,
) -> Option<ToolchainPreflightStatus> {
self.toolchain_preflight
.read()
.await
.get(toolchain)
.cloned()
}
pub async fn set_pressure_assessment(&self, assessment: PressureAssessment) {
*self.pressure_assessment.write().await = assessment;
}
pub async fn pressure_assessment(&self) -> PressureAssessment {
self.pressure_assessment.read().await.clone()
}
pub async fn has_bun(&self) -> bool {
self.capabilities.read().await.has_bun()
}
pub async fn has_node(&self) -> bool {
self.capabilities.read().await.has_node()
}
pub async fn has_rust(&self) -> bool {
self.capabilities.read().await.has_rust()
}
pub async fn has_nix(&self) -> bool {
self.capabilities.read().await.has_nix()
}
pub async fn has_go(&self) -> bool {
self.capabilities.read().await.has_go()
}
pub async fn has_zig(&self) -> bool {
self.capabilities.read().await.has_zig()
}
pub async fn disable(&self, reason: Option<String>) {
*self.drain_completion.write().await = DrainCompletionAction::StayDrained;
self.lifecycle
.write()
.await
.set_admin(AdminIntent::Disabled);
*self.disabled_reason.write().await = reason;
self.disabled_at
.store(current_unix_secs_for_disabled_at(), Ordering::Relaxed);
}
pub async fn drain(&self) {
*self.drain_completion.write().await = DrainCompletionAction::StayDrained;
self.lifecycle
.write()
.await
.set_admin(AdminIntent::Draining);
self.check_drain_complete().await;
}
pub async fn drain_for_removal(&self) {
*self.drain_completion.write().await = DrainCompletionAction::RemoveFromPool;
self.lifecycle
.write()
.await
.set_admin(AdminIntent::Draining);
self.check_drain_complete().await;
}
pub async fn drain_then_disable(&self, reason: Option<String>) {
*self.drain_completion.write().await = DrainCompletionAction::Disable { reason };
self.lifecycle
.write()
.await
.set_admin(AdminIntent::Draining);
self.check_drain_complete().await;
}
pub async fn enable(&self) {
*self.drain_completion.write().await = DrainCompletionAction::StayDrained;
*self.lifecycle.write().await = WorkerLifecycle::new();
*self.disabled_reason.write().await = None;
self.disabled_at.store(0, Ordering::Relaxed);
self.circuit.write().await.close();
*self.last_error_msg.write().await = None;
}
pub async fn is_disabled(&self) -> bool {
self.lifecycle.read().await.admin == AdminIntent::Disabled
}
pub async fn is_draining(&self) -> bool {
self.lifecycle.read().await.admin == AdminIntent::Draining
}
pub async fn is_drained(&self) -> bool {
self.lifecycle.read().await.admin == AdminIntent::Drained
}
pub async fn check_drain_complete(&self) {
let used = self.used_slots.load(Ordering::Acquire);
if used != 0 || self.lifecycle.read().await.admin != AdminIntent::Draining {
return;
}
let completion = self.drain_completion.read().await.clone();
let mut lifecycle = self.lifecycle.write().await;
if lifecycle.admin != AdminIntent::Draining || self.used_slots.load(Ordering::Acquire) != 0
{
return;
}
let disable_reason = match completion {
DrainCompletionAction::StayDrained | DrainCompletionAction::RemoveFromPool => {
lifecycle.set_admin(AdminIntent::Drained);
None
}
DrainCompletionAction::Disable { reason } => {
lifecycle.set_admin(AdminIntent::Disabled);
Some(reason)
}
};
drop(lifecycle);
if let Some(reason) = disable_reason {
*self.disabled_reason.write().await = reason;
self.disabled_at
.store(current_unix_secs_for_disabled_at(), Ordering::Relaxed);
*self.drain_completion.write().await = DrainCompletionAction::StayDrained;
}
}
async fn remove_after_drain_pending(&self) -> bool {
*self.drain_completion.read().await == DrainCompletionAction::RemoveFromPool
}
pub async fn disabled_reason(&self) -> Option<String> {
self.disabled_reason.read().await.clone()
}
pub fn disabled_at(&self) -> Option<i64> {
let v = self.disabled_at.load(Ordering::Relaxed);
if v == 0 { None } else { Some(v) }
}
pub fn used_slots(&self) -> u32 {
self.used_slots.load(Ordering::Relaxed)
}
pub fn restore_slots(&self, count: u32) -> std::io::Result<()> {
self.used_slots
.try_update(Ordering::SeqCst, Ordering::SeqCst, |used| {
used.checked_add(count)
})
.map(|_| ())
.map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
"durable reservation overflow",
)
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolchainPreflightStatus {
pub usable: bool,
pub reason: Option<String>,
pub checked_at_unix_ms: i64,
}
impl ToolchainPreflightStatus {
pub fn is_fresh(&self, ttl: Duration) -> bool {
let age_ms = current_unix_ms().saturating_sub(self.checked_at_unix_ms);
age_ms <= duration_millis_i64(ttl)
}
pub fn is_definitive(&self) -> bool {
self.usable
|| self
.reason
.as_deref()
.is_some_and(|reason| reason.starts_with("toolchain_preflight_command_failed"))
}
pub fn is_reusable(&self, definitive_ttl: Duration, transient_ttl: Duration) -> bool {
self.is_fresh(if self.is_definitive() {
definitive_ttl
} else {
transient_ttl
})
}
}
#[derive(Clone)]
pub struct WorkerPool {
workers: Arc<RwLock<HashMap<WorkerId, Arc<WorkerState>>>>,
worker_count: Arc<AtomicUsize>,
disk_slot_policy: DiskSlotPolicy,
}
impl WorkerPool {
pub fn new() -> Self {
Self::with_selection_config(&rch_common::SelectionConfig::default())
}
pub fn with_selection_config(config: &rch_common::SelectionConfig) -> Self {
Self {
workers: Arc::new(RwLock::new(HashMap::new())),
worker_count: Arc::new(AtomicUsize::new(0)),
disk_slot_policy: DiskSlotPolicy::from(config),
}
}
pub async fn add_worker(&self, config: WorkerConfig) {
let id = config.id.clone();
{
let workers = self.workers.read().await;
if let Some(existing) = workers.get(&id) {
debug!("Updating existing worker: {}", id);
existing.update_config(config).await;
return;
}
}
let state = Arc::new(WorkerState::with_disk_slot_policy(
config,
self.disk_slot_policy,
));
let mut workers = self.workers.write().await;
if let Some(existing) = workers.get(&id) {
let config = state.config.read().await.clone();
existing.update_config(config).await;
} else {
workers.insert(id.clone(), state);
self.worker_count.fetch_add(1, Ordering::SeqCst);
debug!("Added worker: {}", id);
}
}
#[cfg(test)]
pub async fn add_worker_state(&self, state: WorkerState) {
let id = state.config.read().await.id.clone();
let mut workers = self.workers.write().await;
if workers.insert(id.clone(), Arc::new(state)).is_none() {
self.worker_count.fetch_add(1, Ordering::SeqCst);
}
debug!("Added worker: {}", id);
}
pub async fn remove_worker(&self, id: &WorkerId) -> bool {
let mut workers = self.workers.write().await;
if workers.remove(id).is_some() {
self.worker_count.fetch_sub(1, Ordering::SeqCst);
debug!("Removed worker: {}", id);
true
} else {
false
}
}
pub async fn prune_drained(&self) -> usize {
let snapshot: Vec<(WorkerId, Arc<WorkerState>)> = {
let workers = self.workers.read().await;
workers
.iter()
.map(|(id, worker)| (id.clone(), Arc::clone(worker)))
.collect()
};
let mut to_remove = Vec::new();
for (id, worker) in snapshot {
worker.check_drain_complete().await;
let status = worker.status().await;
if status == WorkerStatus::Drained
&& worker.used_slots() == 0
&& worker.remove_after_drain_pending().await
{
to_remove.push((id, worker));
}
}
let mut count = 0;
let mut workers = self.workers.write().await;
for (id, worker) in to_remove {
let still_same_worker = workers
.get(&id)
.is_some_and(|current| Arc::ptr_eq(current, &worker));
if !still_same_worker {
continue;
}
worker.check_drain_complete().await;
let should_remove = worker.status().await == WorkerStatus::Drained
&& worker.used_slots() == 0
&& worker.remove_after_drain_pending().await;
if should_remove && workers.remove(&id).is_some() {
count += 1;
debug!("Pruned drained worker: {}", id);
}
}
if count > 0 {
self.worker_count.fetch_sub(count, Ordering::SeqCst);
}
count
}
pub fn len(&self) -> usize {
self.worker_count.load(Ordering::SeqCst)
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub async fn all_workers(&self) -> Vec<Arc<WorkerState>> {
let workers = self.workers.read().await;
workers.values().cloned().collect()
}
pub async fn healthy_workers(&self) -> Vec<Arc<WorkerState>> {
let workers = self.workers.read().await;
let mut healthy = Vec::new();
for worker in workers.values() {
let status = worker.status().await;
if status == WorkerStatus::Healthy || status == WorkerStatus::Degraded {
healthy.push(worker.clone());
}
}
healthy
}
pub async fn get(&self, id: &WorkerId) -> Option<Arc<WorkerState>> {
let workers = self.workers.read().await;
workers.get(id).cloned()
}
pub async fn set_status(&self, id: &WorkerId, status: WorkerStatus) {
let workers = self.workers.read().await;
if let Some(worker) = workers.get(id) {
worker.set_status(status).await;
debug!("Set {} status to {:?}", id, status);
}
}
pub async fn release_slots(&self, id: &WorkerId, slots: u32) {
let workers = self.workers.read().await;
if let Some(worker) = workers.get(id) {
worker.release_slots(slots).await;
debug!("Released {} slots on worker {}", slots, id);
} else {
debug!("Worker {} not found, cannot release slots", id);
}
}
}
fn current_unix_secs_for_disabled_at() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(duration_secs_i64)
.unwrap_or(1)
.max(1)
}
impl Default for WorkerPool {
fn default() -> Self {
Self::new()
}
}
fn current_unix_ms() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(duration_millis_i64)
.unwrap_or_default()
}
fn refresh_preserved_pressure_age(
assessment: &mut PressureAssessment,
config: &DiskPressurePolicyConfig,
now_ms: i64,
) -> bool {
if !assessment.telemetry_fresh {
return false;
}
let Some(previous_age_secs) = assessment.telemetry_age_secs else {
return false;
};
let elapsed_secs = now_ms
.saturating_sub(assessment.evaluated_at_unix_ms)
.max(0) as u64
/ 1_000;
let age_secs = previous_age_secs.saturating_add(elapsed_secs);
assessment.telemetry_age_secs = Some(age_secs);
assessment.telemetry_fresh = age_secs <= config.telemetry_stale_after.as_secs();
assessment.telemetry_fresh
}
#[derive(Debug, Serialize)]
pub struct WorkerCapabilitiesInfo {
pub id: String,
pub host: String,
pub user: String,
pub capabilities: WorkerCapabilities,
pub pressure_assessment: PressureAssessment,
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh: Option<WorkerCapabilitiesRefreshInfo>,
}
#[derive(Debug, Clone, Serialize)]
pub struct WorkerCapabilitiesRefreshInfo {
pub attempted: bool,
pub live: bool,
pub source: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
impl WorkerCapabilitiesRefreshInfo {
fn live_probe() -> Self {
Self {
attempted: true,
live: true,
source: "live_probe".to_string(),
message: None,
}
}
fn cached_after_probe_failure(message: impl Into<String>) -> Self {
Self {
attempted: true,
live: false,
source: "cached_after_probe_failure".to_string(),
message: Some(message.into()),
}
}
}
#[derive(Debug, Serialize)]
pub struct WorkerCapabilitiesResponse {
pub workers: Vec<WorkerCapabilitiesInfo>,
}
#[derive(Debug, Serialize)]
pub struct WorkerStateResponse {
pub status: String,
pub worker_id: String,
pub action: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_status: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub active_slots: Option<u32>,
}
pub async fn get_workers_capabilities(
ctx: &DaemonContext,
refresh: bool,
) -> WorkerCapabilitiesResponse {
let workers = ctx.pool.all_workers().await;
let mut entries = Vec::with_capacity(workers.len());
let mut refresh_results = if refresh {
refresh_worker_capabilities_concurrently(&workers).await
} else {
HashMap::new()
};
for worker in workers {
let (id, host, user) = {
let config = worker.config.read().await;
(
config.id.to_string(),
config.host.clone(),
config.user.clone(),
)
};
let capabilities = worker.capabilities().await;
let pressure_assessment = worker.pressure_assessment().await;
let refresh = refresh_results.remove(&id);
entries.push(WorkerCapabilitiesInfo {
id,
host,
user,
capabilities,
pressure_assessment,
refresh,
});
}
WorkerCapabilitiesResponse { workers: entries }
}
async fn refresh_worker_capabilities_concurrently(
workers: &[Arc<WorkerState>],
) -> HashMap<String, WorkerCapabilitiesRefreshInfo> {
let mut handles = Vec::with_capacity(workers.len());
for worker in workers {
let id = {
let config = worker.config.read().await;
config.id.to_string()
};
let worker = Arc::clone(worker);
handles.push((
id,
tokio::spawn(async move { refresh_worker_capabilities_for_worker(worker).await }),
));
}
let mut results = HashMap::with_capacity(handles.len());
for (id, handle) in handles {
let refresh = match handle.await {
Ok(refresh) => refresh,
Err(err) => WorkerCapabilitiesRefreshInfo::cached_after_probe_failure(format!(
"capabilities refresh task failed: {err}; returning cached capability snapshot"
)),
};
results.insert(id, refresh);
}
results
}
async fn refresh_worker_capabilities_for_worker(
worker: Arc<WorkerState>,
) -> WorkerCapabilitiesRefreshInfo {
let probe = tokio::time::timeout(
CAPABILITIES_REFRESH_WORKER_BUDGET,
probe_worker_capabilities(&worker, CAPABILITIES_REFRESH_PROBE_TIMEOUT, None),
)
.await;
match probe {
Ok(Some(capabilities)) => {
worker.set_capabilities(capabilities).await;
WorkerCapabilitiesRefreshInfo::live_probe()
}
Ok(None) => WorkerCapabilitiesRefreshInfo::cached_after_probe_failure(
"capabilities probe failed; returning cached capability snapshot",
),
Err(_) => WorkerCapabilitiesRefreshInfo::cached_after_probe_failure(format!(
"capabilities probe exceeded {}s response budget; returning cached capability snapshot",
CAPABILITIES_REFRESH_WORKER_BUDGET.as_secs()
)),
}
}
pub async fn handle_worker_drain(ctx: &DaemonContext, worker_id: &WorkerId) -> WorkerStateResponse {
match ctx.pool.get(worker_id).await {
Some(worker) => {
let active_slots = worker.used_slots();
worker.drain().await;
let new_status = worker.status().await;
WorkerStateResponse {
status: "ok".to_string(),
worker_id: worker_id.to_string(),
action: "drain".to_string(),
new_status: Some(worker_status_label(new_status).to_string()),
reason: None,
message: Some(format!(
"Worker is {}. {} slot(s) currently in use.",
worker_status_label(new_status),
active_slots,
)),
active_slots: Some(active_slots),
}
}
None => WorkerStateResponse {
status: "error".to_string(),
worker_id: worker_id.to_string(),
action: "drain".to_string(),
new_status: None,
reason: None,
message: Some(format!("Worker '{}' not found", worker_id)),
active_slots: None,
},
}
}
pub async fn handle_worker_enable(
ctx: &DaemonContext,
worker_id: &WorkerId,
) -> WorkerStateResponse {
match ctx.pool.get(worker_id).await {
Some(worker) => {
let old_status = worker.status().await;
worker.enable().await;
if let Some(store) = &ctx.bypass_store {
let _ = store.lock().await.remove(worker_id.as_str());
}
if let Some(store) = &ctx.admin_disable_store
&& let Err(e) = store.lock().await.remove(worker_id.as_str())
{
tracing::warn!(
"Failed to remove durable admin-disable record for {}: {}",
worker_id,
e
);
}
WorkerStateResponse {
status: "ok".to_string(),
worker_id: worker_id.to_string(),
action: "enable".to_string(),
new_status: Some("healthy".to_string()),
reason: None,
message: Some(format!("Worker enabled (was {:?})", old_status)),
active_slots: None,
}
}
None => WorkerStateResponse {
status: "error".to_string(),
worker_id: worker_id.to_string(),
action: "enable".to_string(),
new_status: None,
reason: None,
message: Some(format!("Worker '{}' not found", worker_id)),
active_slots: None,
},
}
}
pub async fn handle_worker_disable(
ctx: &DaemonContext,
worker_id: &WorkerId,
reason: Option<String>,
drain_first: bool,
) -> WorkerStateResponse {
match ctx.pool.get(worker_id).await {
Some(worker) => {
let active_slots = worker.used_slots();
if let Some(store) = &ctx.admin_disable_store {
let record = rch_common::bypass_record::AdminDisableRecord {
worker_id: worker_id.to_string(),
reason: reason.clone(),
disabled_unix_ms: u64::try_from(chrono::Utc::now().timestamp_millis())
.unwrap_or(0),
};
if let Err(e) = store.lock().await.upsert(record) {
tracing::warn!(
"Failed to persist durable admin-disable record for {}: {}",
worker_id,
e
);
}
}
if drain_first && active_slots > 0 {
worker.drain_then_disable(reason.clone()).await;
let new_status = worker.status().await;
WorkerStateResponse {
status: "ok".to_string(),
worker_id: worker_id.to_string(),
action: "disable".to_string(),
new_status: Some(worker_status_label(new_status).to_string()),
reason: reason.clone(),
message: Some(format!(
"Worker is {} before disable. {} slot(s) in use. Reason: {}",
worker_status_label(new_status),
active_slots,
reason.as_deref().unwrap_or("none provided")
)),
active_slots: Some(active_slots),
}
} else {
worker.disable(reason.clone()).await;
WorkerStateResponse {
status: "ok".to_string(),
worker_id: worker_id.to_string(),
action: "disable".to_string(),
new_status: Some("disabled".to_string()),
reason: reason.clone(),
message: Some(format!(
"Worker disabled. Reason: {}",
reason.as_deref().unwrap_or("none provided")
)),
active_slots: Some(active_slots),
}
}
}
None => WorkerStateResponse {
status: "error".to_string(),
worker_id: worker_id.to_string(),
action: "disable".to_string(),
new_status: None,
reason,
message: Some(format!("Worker '{}' not found", worker_id)),
active_slots: None,
},
}
}
fn worker_status_label(status: WorkerStatus) -> &'static str {
match status {
WorkerStatus::Healthy => "healthy",
WorkerStatus::Degraded => "degraded",
WorkerStatus::Unreachable => "unreachable",
WorkerStatus::Draining => "draining",
WorkerStatus::Drained => "drained",
WorkerStatus::Disabled => "disabled",
}
}
#[cfg(test)]
mod tests {
use super::*;
const ALL_FAILURE_CLASSES: &[BypassFailureClass] = &[
BypassFailureClass::Ssh,
BypassFailureClass::WorkerBinary,
BypassFailureClass::RuntimeToolchain,
BypassFailureClass::DiskInodePressure,
BypassFailureClass::StaleTelemetry,
BypassFailureClass::PathSync,
BypassFailureClass::ArtifactRetrieval,
BypassFailureClass::CircuitBreaker,
BypassFailureClass::OsArchMismatch,
];
#[test]
fn lifecycle_new_is_active_healthy_and_schedulable() {
let lc = WorkerLifecycle::new();
assert_eq!(lc.admin, AdminIntent::Active);
assert_eq!(lc.eligibility, EligibilityState::Healthy);
assert_eq!(lc.bypass_cause, None);
assert!(lc.is_schedulable());
assert!(!lc.is_canary_pending());
assert_eq!(WorkerLifecycle::default(), lc);
}
#[test]
fn lifecycle_from_worker_status_maps_both_axes() {
let cases = [
(
WorkerStatus::Healthy,
AdminIntent::Active,
EligibilityState::Healthy,
),
(
WorkerStatus::Degraded,
AdminIntent::Active,
EligibilityState::Degraded,
),
(
WorkerStatus::Unreachable,
AdminIntent::Active,
EligibilityState::Unreachable,
),
(
WorkerStatus::Draining,
AdminIntent::Draining,
EligibilityState::Healthy,
),
(
WorkerStatus::Drained,
AdminIntent::Drained,
EligibilityState::Healthy,
),
(
WorkerStatus::Disabled,
AdminIntent::Disabled,
EligibilityState::Healthy,
),
];
for (status, admin, eligibility) in cases {
let lc = WorkerLifecycle::from_worker_status(status);
assert_eq!(lc.admin, admin, "admin axis for {status:?}");
assert_eq!(
lc.eligibility, eligibility,
"eligibility axis for {status:?}"
);
}
}
#[test]
fn lifecycle_legacy_status_collapses_two_axes() {
let disabled = WorkerLifecycle {
admin: AdminIntent::Disabled,
eligibility: EligibilityState::Healthy,
bypass_cause: None,
};
assert_eq!(disabled.legacy_status(), WorkerStatus::Disabled);
let bypassed = WorkerLifecycle {
admin: AdminIntent::Active,
eligibility: EligibilityState::TemporaryBypass,
bypass_cause: Some(BypassFailureClass::Ssh),
};
assert_eq!(bypassed.legacy_status(), WorkerStatus::Unreachable);
let canary = WorkerLifecycle {
admin: AdminIntent::Active,
eligibility: EligibilityState::RecoveredPendingCanary,
bypass_cause: None,
};
assert_eq!(canary.legacy_status(), WorkerStatus::Unreachable);
for status in [
WorkerStatus::Healthy,
WorkerStatus::Degraded,
WorkerStatus::Disabled,
] {
assert_eq!(
WorkerLifecycle::from_worker_status(status).legacy_status(),
status
);
}
}
#[test]
fn lifecycle_is_schedulable_matrix() {
for elig in [EligibilityState::Healthy, EligibilityState::Degraded] {
let lc = WorkerLifecycle {
admin: AdminIntent::Active,
eligibility: elig,
bypass_cause: None,
};
assert!(lc.is_schedulable(), "Active+{elig:?} must schedule");
}
for elig in [
EligibilityState::Unreachable,
EligibilityState::TemporaryBypass,
EligibilityState::RecoveredPendingCanary,
] {
let lc = WorkerLifecycle {
admin: AdminIntent::Active,
eligibility: elig,
bypass_cause: None,
};
assert!(!lc.is_schedulable(), "Active+{elig:?} must NOT schedule");
}
for admin in [
AdminIntent::Draining,
AdminIntent::Drained,
AdminIntent::Disabled,
] {
let lc = WorkerLifecycle {
admin,
eligibility: EligibilityState::Healthy,
bypass_cause: None,
};
assert!(!lc.is_schedulable(), "{admin:?}+Healthy must NOT schedule");
}
}
#[test]
fn lifecycle_enter_bypass_quarantines_for_every_failure_class_without_touching_admin() {
for &cause in ALL_FAILURE_CLASSES {
let mut lc = WorkerLifecycle::new();
let admin_before = lc.admin;
lc.enter_bypass(cause);
assert_eq!(lc.eligibility, EligibilityState::TemporaryBypass);
assert_eq!(lc.bypass_cause, Some(cause), "cause recorded for {cause:?}");
assert_eq!(lc.admin, admin_before, "admin axis must be untouched");
assert!(!lc.is_schedulable());
}
}
#[test]
fn lifecycle_legal_recovery_path_healthy_again() {
let mut lc = WorkerLifecycle::new();
lc.enter_bypass(BypassFailureClass::DiskInodePressure);
lc.recover_to_canary().expect("probe success -> canary");
assert_eq!(lc.eligibility, EligibilityState::RecoveredPendingCanary);
assert_eq!(lc.bypass_cause, None, "cause cleared on recovery");
assert!(lc.is_canary_pending());
assert!(
!lc.is_schedulable(),
"canary-pending is not normal scheduling"
);
lc.promote_from_canary().expect("canary success -> healthy");
assert_eq!(lc.eligibility, EligibilityState::Healthy);
assert!(lc.is_schedulable());
}
#[test]
fn lifecycle_canary_relapse_via_enter_bypass_is_legal() {
let mut lc = WorkerLifecycle::new();
lc.enter_bypass(BypassFailureClass::Ssh);
lc.recover_to_canary().unwrap();
lc.enter_bypass(BypassFailureClass::PathSync);
assert_eq!(lc.eligibility, EligibilityState::TemporaryBypass);
assert_eq!(lc.bypass_cause, Some(BypassFailureClass::PathSync));
}
#[test]
fn lifecycle_illegal_recover_to_canary_from_non_bypass() {
for elig in [
EligibilityState::Healthy,
EligibilityState::Degraded,
EligibilityState::Unreachable,
EligibilityState::RecoveredPendingCanary,
] {
let mut lc = WorkerLifecycle {
admin: AdminIntent::Active,
eligibility: elig,
bypass_cause: None,
};
let err = lc
.recover_to_canary()
.expect_err("recover_to_canary must be illegal from non-bypass");
assert_eq!(err.from, elig);
assert_eq!(err.to, EligibilityState::RecoveredPendingCanary);
assert_eq!(lc.eligibility, elig);
}
}
#[test]
fn lifecycle_illegal_promote_from_non_canary() {
for elig in [
EligibilityState::Healthy,
EligibilityState::Degraded,
EligibilityState::Unreachable,
EligibilityState::TemporaryBypass,
] {
let mut lc = WorkerLifecycle {
admin: AdminIntent::Active,
eligibility: elig,
bypass_cause: None,
};
let err = lc
.promote_from_canary()
.expect_err("promote must be illegal from non-canary");
assert_eq!(err.from, elig);
assert_eq!(lc.eligibility, elig, "state unchanged on illegal promote");
}
}
#[test]
fn lifecycle_observe_health_never_clears_quarantine() {
let mut lc = WorkerLifecycle::new();
lc.enter_bypass(BypassFailureClass::StaleTelemetry);
assert!(!lc.observe_health(EligibilityState::Healthy));
assert_eq!(lc.eligibility, EligibilityState::TemporaryBypass);
assert_eq!(lc.bypass_cause, Some(BypassFailureClass::StaleTelemetry));
lc.recover_to_canary().unwrap();
assert!(!lc.observe_health(EligibilityState::Healthy));
assert_eq!(lc.eligibility, EligibilityState::RecoveredPendingCanary);
}
#[test]
fn lifecycle_observe_health_moves_among_plain_states() {
let mut lc = WorkerLifecycle::new();
assert!(lc.observe_health(EligibilityState::Degraded));
assert_eq!(lc.eligibility, EligibilityState::Degraded);
assert!(lc.observe_health(EligibilityState::Unreachable));
assert_eq!(lc.eligibility, EligibilityState::Unreachable);
assert!(lc.observe_health(EligibilityState::Healthy));
assert_eq!(lc.eligibility, EligibilityState::Healthy);
assert!(!lc.observe_health(EligibilityState::TemporaryBypass));
assert_eq!(lc.eligibility, EligibilityState::Healthy);
}
#[test]
fn lifecycle_operator_disabled_worker_never_auto_rejoins() {
let mut lc = WorkerLifecycle::new();
lc.set_admin(AdminIntent::Disabled);
lc.enter_bypass(BypassFailureClass::CircuitBreaker);
lc.recover_to_canary().unwrap();
lc.promote_from_canary().unwrap();
assert_eq!(lc.eligibility, EligibilityState::Healthy);
assert_eq!(lc.admin, AdminIntent::Disabled);
assert!(!lc.is_schedulable());
assert!(!lc.is_canary_pending());
}
#[test]
fn lifecycle_admin_axis_is_invariant_across_eligibility_transitions() {
for admin in [
AdminIntent::Active,
AdminIntent::Draining,
AdminIntent::Drained,
AdminIntent::Disabled,
] {
let mut lc = WorkerLifecycle {
admin,
eligibility: EligibilityState::Healthy,
bypass_cause: None,
};
lc.enter_bypass(BypassFailureClass::OsArchMismatch);
lc.recover_to_canary().unwrap();
lc.promote_from_canary().unwrap();
lc.observe_health(EligibilityState::Degraded);
assert_eq!(
lc.admin, admin,
"eligibility transitions must not move admin"
);
}
}
#[test]
fn lifecycle_serde_round_trips_full_state() {
let lc = WorkerLifecycle {
admin: AdminIntent::Draining,
eligibility: EligibilityState::TemporaryBypass,
bypass_cause: Some(BypassFailureClass::ArtifactRetrieval),
};
let json = serde_json::to_string(&lc).expect("serialize");
assert!(json.contains("temporary_bypass"));
assert!(json.contains("artifact_retrieval"));
let back: WorkerLifecycle = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, lc);
let healthy = WorkerLifecycle::new();
let json = serde_json::to_string(&healthy).expect("serialize");
assert!(!json.contains("bypass_cause"), "None cause must be omitted");
}
#[test]
fn lifecycle_every_failure_class_serializes_to_distinct_snake_case_token() {
let mut seen = std::collections::BTreeSet::new();
for &cause in ALL_FAILURE_CLASSES {
let token = serde_json::to_string(&cause).expect("serialize cause");
assert!(seen.insert(token.clone()), "duplicate token {token}");
let back: BypassFailureClass = serde_json::from_str(&token).expect("deserialize cause");
assert_eq!(back, cause);
}
assert_eq!(seen.len(), ALL_FAILURE_CLASSES.len());
}
#[test]
fn test_duration_millis_i64_saturates() {
assert_eq!(
duration_millis_i64(Duration::from_millis(u64::MAX)),
i64::MAX
);
}
#[test]
fn test_toolchain_preflight_freshness_saturates_extreme_ttl() {
let status = ToolchainPreflightStatus {
usable: true,
reason: None,
checked_at_unix_ms: 0,
};
assert!(status.is_fresh(Duration::from_millis(u64::MAX)));
}
#[test]
fn test_toolchain_preflight_transport_failures_expire_quickly() {
let long = Duration::from_secs(600);
let short = Duration::from_secs(60);
let two_minutes_ago = current_unix_ms() - 120_000;
let verdict = |usable: bool, reason: Option<&str>| ToolchainPreflightStatus {
usable,
reason: reason.map(str::to_string),
checked_at_unix_ms: two_minutes_ago,
};
let broken = verdict(
false,
Some("toolchain_preflight_command_failed:1:no such toolchain"),
);
assert!(broken.is_definitive());
assert!(broken.is_reusable(long, short));
let unreachable = verdict(false, Some("toolchain_preflight_connect_failed:timed out"));
assert!(!unreachable.is_definitive());
assert!(!unreachable.is_reusable(long, short));
let healthy = verdict(true, None);
assert!(healthy.is_definitive());
assert!(healthy.is_reusable(long, short));
}
fn test_config(id: &str) -> WorkerConfig {
WorkerConfig {
id: WorkerId::new(id),
host: "localhost".to_string(),
user: "user".to_string(),
identity_file: "~/.ssh/id_rsa".to_string(),
total_slots: 8,
priority: 100,
tags: vec![],
tools: Vec::new(),
}
}
#[tokio::test]
#[serial_test::serial]
async fn test_refresh_worker_capabilities_marks_live_probe() {
let _guard = rch_common::test_guard!();
struct MockOverrideGuard;
impl Drop for MockOverrideGuard {
fn drop(&mut self) {
rch_common::mock::clear_mock_overrides();
}
}
rch_common::mock::set_mock_enabled_override(Some(true));
let _mock_guard = MockOverrideGuard;
let state = std::sync::Arc::new(WorkerState::new(test_config("test-worker")));
let refresh = refresh_worker_capabilities_for_worker(std::sync::Arc::clone(&state)).await;
assert!(refresh.attempted);
assert!(refresh.live);
assert_eq!(refresh.source, "live_probe");
assert!(state.has_rust().await);
}
#[tokio::test]
async fn test_worker_state_new_defaults() {
let state = WorkerState::new(test_config("test-worker"));
assert_eq!(state.status().await, WorkerStatus::Healthy);
assert_eq!(state.available_slots().await, 8);
assert_eq!(state.used_slots(), 0);
assert_eq!(state.get_speed_score(), 50.0);
assert!(state.last_latency_ms().is_none());
assert!(state.last_error().await.is_none());
assert!(!state.is_disabled().await);
assert!(!state.is_draining().await);
assert!(state.disabled_reason().await.is_none());
assert!(state.disabled_at().is_none());
}
#[test]
fn test_speed_score_roundtrip_normal_values() {
let state = WorkerState::new(test_config("test"));
assert_eq!(state.get_speed_score(), 50.0);
state.set_speed_score(0.0);
assert_eq!(state.get_speed_score(), 0.0);
state.set_speed_score(1.0);
assert_eq!(state.get_speed_score(), 1.0);
state.set_speed_score(100.0);
assert_eq!(state.get_speed_score(), 100.0);
state.set_speed_score(99.5);
assert_eq!(state.get_speed_score(), 99.5);
state.set_speed_score(-1.0);
assert_eq!(state.get_speed_score(), -1.0);
}
#[test]
fn test_speed_score_roundtrip_extremes() {
let state = WorkerState::new(test_config("test"));
state.set_speed_score(f64::MAX);
assert_eq!(state.get_speed_score(), f64::MAX);
state.set_speed_score(f64::MIN);
assert_eq!(state.get_speed_score(), f64::MIN);
state.set_speed_score(f64::MIN_POSITIVE);
assert_eq!(state.get_speed_score(), f64::MIN_POSITIVE);
state.set_speed_score(f64::EPSILON);
assert_eq!(state.get_speed_score(), f64::EPSILON);
}
#[test]
fn test_speed_score_roundtrip_special_float_values() {
let state = WorkerState::new(test_config("test"));
state.set_speed_score(f64::INFINITY);
assert_eq!(state.get_speed_score(), f64::INFINITY);
state.set_speed_score(f64::NEG_INFINITY);
assert_eq!(state.get_speed_score(), f64::NEG_INFINITY);
state.set_speed_score(f64::NAN);
let retrieved = state.get_speed_score();
assert!(
retrieved.is_nan(),
"NaN should roundtrip via to_bits/from_bits"
);
assert_eq!(
f64::NAN.to_bits(),
retrieved.to_bits(),
"NaN bit pattern should be preserved"
);
state.set_speed_score(-0.0_f64);
let retrieved = state.get_speed_score();
assert_eq!(retrieved, 0.0); assert_eq!((-0.0_f64).to_bits(), retrieved.to_bits());
}
#[test]
fn test_last_latency_ms_none_default() {
let state = WorkerState::new(test_config("test"));
assert_eq!(state.last_latency_ms(), None);
}
#[test]
fn test_last_latency_ms_roundtrip() {
let state = WorkerState::new(test_config("test"));
state.set_last_latency_ms(Some(100));
assert_eq!(state.last_latency_ms(), Some(100));
state.set_last_latency_ms(Some(250));
assert_eq!(state.last_latency_ms(), Some(250));
state.set_last_latency_ms(Some(u64::MAX));
assert_eq!(state.last_latency_ms(), Some(u64::MAX));
state.set_last_latency_ms(None);
assert_eq!(state.last_latency_ms(), None);
}
#[test]
fn test_last_latency_ms_zero_sentinel_behavior() {
let state = WorkerState::new(test_config("test"));
state.set_last_latency_ms(Some(0));
assert_eq!(
state.last_latency_ms(),
None,
"Some(0) should map to None (0 is sentinel for None)"
);
state.set_last_latency_ms(Some(1));
assert_eq!(state.last_latency_ms(), Some(1));
}
#[test]
fn test_disabled_at_none_default() {
let state = WorkerState::new(test_config("test"));
assert_eq!(state.disabled_at(), None);
}
#[tokio::test]
async fn test_disabled_at_set_on_disable() {
let state = WorkerState::new(test_config("test"));
assert!(state.disabled_at().is_none());
state.disable(Some("test reason".to_string())).await;
let ts = state.disabled_at();
assert!(ts.is_some(), "disabled_at should be set after disable()");
assert!(ts.unwrap() > 0, "timestamp should be positive");
}
#[tokio::test]
async fn test_disabled_at_cleared_on_enable() {
let state = WorkerState::new(test_config("test"));
state.disable(Some("test".to_string())).await;
assert!(state.disabled_at().is_some());
state.enable().await;
assert_eq!(
state.disabled_at(),
None,
"disabled_at should be None after enable()"
);
}
#[tokio::test]
async fn test_record_health_check_drives_authoritative_circuit() {
let state = WorkerState::new(test_config("test"));
let cfg = CircuitBreakerConfig {
failure_threshold: 3,
success_threshold: 2,
open_cooldown_secs: 0,
..Default::default()
};
assert_eq!(state.circuit_state().await, Some(CircuitState::Closed));
for _ in 0..3 {
state.record_health_check(false, false, &cfg).await;
}
assert_ne!(state.circuit_state().await, Some(CircuitState::Closed));
state.record_health_check(true, false, &cfg).await;
let (_, new) = state.record_health_check(true, false, &cfg).await;
assert_eq!(new, CircuitState::Closed);
assert_eq!(state.circuit_state().await, Some(CircuitState::Closed));
}
#[tokio::test]
async fn test_enable_resets_authoritative_circuit() {
let state = WorkerState::new(test_config("test"));
state.open_circuit().await;
state.set_error("boom".to_string()).await;
assert_eq!(state.circuit_state().await, Some(CircuitState::Open));
state.enable().await;
assert_eq!(
state.circuit_state().await,
Some(CircuitState::Closed),
"enable() must reset the authoritative circuit to Closed"
);
assert_eq!(state.last_error().await, None);
}
#[tokio::test]
async fn test_speed_score_concurrent_updates() {
let state = Arc::new(WorkerState::new(test_config("test")));
let mut handles = vec![];
for i in 0..10u64 {
let s = Arc::clone(&state);
handles.push(tokio::spawn(async move {
s.set_speed_score(i as f64 * 10.0);
}));
}
for h in handles {
h.await.unwrap();
}
let score = state.get_speed_score();
assert!(
(0.0..=90.0).contains(&score) && score % 10.0 == 0.0,
"Final speed score {} should be one of the written values",
score
);
}
#[tokio::test]
async fn test_latency_concurrent_updates() {
let state = Arc::new(WorkerState::new(test_config("test")));
let mut handles = vec![];
for i in 0..10u64 {
let s = Arc::clone(&state);
handles.push(tokio::spawn(async move {
if i % 2 == 0 {
s.set_last_latency_ms(Some(i * 100 + 1)); } else {
s.set_last_latency_ms(None);
}
}));
}
for h in handles {
h.await.unwrap();
}
let latency = state.last_latency_ms();
match latency {
None => {} Some(v) => assert!(v <= 901, "Latency {} out of expected range", v),
}
}
#[tokio::test]
async fn test_disabled_at_concurrent_disable_enable() {
let state = Arc::new(WorkerState::new(test_config("test")));
let mut handles = vec![];
for i in 0..10u32 {
let s = Arc::clone(&state);
handles.push(tokio::spawn(async move {
if i % 2 == 0 {
s.disable(Some(format!("reason {i}"))).await;
} else {
s.enable().await;
}
}));
}
for h in handles {
h.await.unwrap();
}
let is_disabled = state.is_disabled().await;
let disabled_at = state.disabled_at();
if is_disabled {
assert!(
disabled_at.is_some(),
"disabled worker should have disabled_at set"
);
}
}
#[tokio::test]
async fn test_slot_reservation() {
let config = WorkerConfig {
id: WorkerId::new("test"),
host: "localhost".to_string(),
user: "user".to_string(),
identity_file: "~/.ssh/id_rsa".to_string(),
total_slots: 8,
priority: 100,
tags: vec![],
tools: Vec::new(),
};
let state = WorkerState::new(config);
assert_eq!(state.available_slots().await, 8);
assert!(state.reserve_slots(4).await);
assert_eq!(state.available_slots().await, 4);
assert!(state.reserve_slots(4).await);
assert_eq!(state.available_slots().await, 0);
assert!(!state.reserve_slots(1).await);
state.release_slots(4).await;
assert_eq!(state.available_slots().await, 4);
}
#[tokio::test]
async fn test_disk_slots_derate_at_floor_and_recover() {
let state = WorkerState::new(test_config("disk-slots"));
assert_eq!(state.effective_total_slots().await, 8);
for (free_gb, expected) in [
(100.0, 8),
(80.0, 7),
(40.0, 3),
(20.0, 1),
(19.9, 0),
(10.0, 0),
(0.0, 0),
(-1.0, 0),
(f64::MAX, 8),
] {
state
.set_pressure_assessment(PressureAssessment {
disk_free_gb: Some(free_gb),
..Default::default()
})
.await;
assert_eq!(
state.effective_total_slots().await,
expected,
"free GiB={free_gb}"
);
assert_eq!(state.available_slots().await, expected);
assert_eq!(state.config.read().await.total_slots, 8);
}
for unknown in [None, Some(f64::NAN), Some(f64::INFINITY)] {
state
.set_pressure_assessment(PressureAssessment {
disk_free_gb: unknown,
..Default::default()
})
.await;
assert_eq!(state.effective_total_slots().await, 8);
}
}
#[tokio::test]
async fn test_disk_slots_keep_active_reservations_when_capacity_shrinks() {
let state = WorkerState::new(test_config("disk-shrink"));
assert!(state.reserve_slots(6).await);
state
.set_pressure_assessment(PressureAssessment {
disk_free_gb: Some(20.0),
..Default::default()
})
.await;
assert_eq!(state.effective_total_slots().await, 1);
assert_eq!(state.used_slots(), 6);
assert_eq!(state.available_slots().await, 0);
assert!(!state.reserve_slots(1).await);
state.release_slots(2).await;
assert_eq!(state.used_slots(), 4);
assert_eq!(state.available_slots().await, 0);
state
.set_pressure_assessment(PressureAssessment {
disk_free_gb: Some(70.0),
..Default::default()
})
.await;
assert_eq!(state.available_slots().await, 2);
state.release_slots(4).await;
assert_eq!(state.available_slots().await, 6);
}
#[tokio::test]
async fn test_disk_slots_budget_survives_worker_reload_and_addition() {
let config = rch_common::SelectionConfig {
min_free_gb: Some(5.0),
disk_gb_per_slot: 2.5,
..Default::default()
};
let pool = WorkerPool::with_selection_config(&config);
pool.add_worker(test_config("first")).await;
let first = pool.get(&WorkerId::new("first")).await.unwrap();
first
.set_pressure_assessment(PressureAssessment {
disk_free_gb: Some(12.5),
..Default::default()
})
.await;
assert_eq!(first.effective_total_slots().await, 3);
let mut reloaded = test_config("first");
reloaded.total_slots = 2;
pool.add_worker(reloaded).await;
assert_eq!(first.effective_total_slots().await, 2);
pool.add_worker(test_config("later")).await;
let later = pool.get(&WorkerId::new("later")).await.unwrap();
later
.set_pressure_assessment(first.pressure_assessment().await)
.await;
assert_eq!(later.effective_total_slots().await, 3);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_disk_slots_concurrent_reservations_respect_capacity() {
let state = Arc::new(WorkerState::new(test_config("disk-race")));
state
.set_pressure_assessment(PressureAssessment {
disk_free_gb: Some(40.0),
..Default::default()
})
.await;
let barrier = Arc::new(tokio::sync::Barrier::new(16));
let mut tasks = Vec::new();
for _ in 0..16 {
let worker = state.clone();
let barrier = barrier.clone();
tasks.push(tokio::spawn(async move {
barrier.wait().await;
worker.reserve_slots(1).await
}));
}
let mut reserved = 0;
for task in tasks {
reserved += u32::from(task.await.unwrap());
}
assert_eq!(reserved, 3);
assert_eq!(state.used_slots(), 3);
assert_eq!(state.available_slots().await, 0);
state.release_slots(reserved).await;
assert_eq!(state.available_slots().await, 3);
}
#[tokio::test]
async fn test_slot_reservation_rejects_integer_overflow() {
let mut config = test_config("slot-overflow");
config.total_slots = u32::MAX;
let state = WorkerState::new(config);
assert!(state.reserve_slots(u32::MAX).await);
assert!(!state.reserve_slots(1).await);
assert_eq!(state.used_slots(), u32::MAX);
}
#[tokio::test]
async fn test_zero_slot_reservation_is_rejected() {
let state = WorkerState::new(test_config("test"));
assert!(!state.reserve_slots(0).await);
assert_eq!(state.used_slots(), 0);
assert_eq!(state.available_slots().await, 8);
}
#[tokio::test]
async fn test_slot_reservation_exact_boundary() {
let state = WorkerState::new(test_config("test"));
assert!(state.reserve_slots(8).await);
assert_eq!(state.available_slots().await, 0);
assert_eq!(state.used_slots(), 8);
assert!(!state.reserve_slots(1).await);
state.release_slots(8).await;
assert_eq!(state.available_slots().await, 8);
}
#[tokio::test]
async fn test_release_slots_more_than_used() {
let state = WorkerState::new(test_config("test"));
assert!(state.reserve_slots(2).await);
assert_eq!(state.used_slots(), 2);
state.release_slots(10).await;
assert_eq!(state.used_slots(), 0);
assert_eq!(state.available_slots().await, 8);
}
#[tokio::test]
async fn test_status_transitions() {
let state = WorkerState::new(test_config("test"));
assert_eq!(state.status().await, WorkerStatus::Healthy);
state.set_status(WorkerStatus::Degraded).await;
assert_eq!(state.status().await, WorkerStatus::Degraded);
state.set_status(WorkerStatus::Unreachable).await;
assert_eq!(state.status().await, WorkerStatus::Unreachable);
state.set_status(WorkerStatus::Healthy).await;
assert_eq!(state.status().await, WorkerStatus::Healthy);
}
#[tokio::test]
async fn test_reserve_slots_refuses_draining_worker() {
let state = WorkerState::new(test_config("drain-race"));
assert!(state.reserve_slots(1).await, "fresh worker should accept");
state.release_slots(1).await;
state.drain().await;
assert!(
!state.reserve_slots(1).await,
"draining worker must refuse new reservations"
);
assert_eq!(state.used_slots(), 0);
state.check_drain_complete().await;
assert_eq!(state.status().await, WorkerStatus::Drained);
assert!(
!state.reserve_slots(1).await,
"drained worker must refuse new reservations"
);
state.disable(Some("maintenance".to_string())).await;
assert!(
!state.reserve_slots(1).await,
"disabled worker must refuse new reservations"
);
}
#[tokio::test]
async fn test_apply_health_status_updates_non_administrative_states() {
let state = WorkerState::new(test_config("test"));
assert_eq!(
state.apply_health_status(WorkerStatus::Degraded).await,
WorkerStatus::Degraded
);
assert_eq!(state.status().await, WorkerStatus::Degraded);
assert_eq!(
state.apply_health_status(WorkerStatus::Unreachable).await,
WorkerStatus::Unreachable
);
assert_eq!(state.status().await, WorkerStatus::Unreachable);
assert_eq!(
state.apply_health_status(WorkerStatus::Healthy).await,
WorkerStatus::Healthy
);
assert_eq!(state.status().await, WorkerStatus::Healthy);
}
#[tokio::test]
async fn test_apply_health_status_preserves_administrative_states() {
let state = WorkerState::new(test_config("test"));
assert!(state.reserve_slots(1).await);
state.drain().await;
assert_eq!(
state.apply_health_status(WorkerStatus::Healthy).await,
WorkerStatus::Draining
);
assert_eq!(state.status().await, WorkerStatus::Draining);
state.release_slots(1).await;
assert_eq!(state.status().await, WorkerStatus::Drained);
assert_eq!(
state.apply_health_status(WorkerStatus::Healthy).await,
WorkerStatus::Drained
);
assert_eq!(state.status().await, WorkerStatus::Drained);
state.disable(Some("maintenance".to_string())).await;
assert_eq!(
state.apply_health_status(WorkerStatus::Healthy).await,
WorkerStatus::Disabled
);
assert_eq!(state.status().await, WorkerStatus::Disabled);
}
#[tokio::test]
async fn test_enter_bypass_excludes_from_scheduling() {
let state = WorkerState::new(test_config("bypassed"));
assert!(state.reserve_slots(1).await);
state.release_slots(1).await;
state.enter_bypass(BypassFailureClass::Ssh).await;
assert_eq!(state.eligibility().await, EligibilityState::TemporaryBypass);
assert_eq!(state.status().await, WorkerStatus::Unreachable);
assert!(!state.lifecycle().await.is_schedulable());
assert_eq!(
state.lifecycle().await.bypass_cause,
Some(BypassFailureClass::Ssh)
);
assert!(
!state.reserve_slots(1).await,
"a bypassed worker must refuse new reservations"
);
}
#[tokio::test]
async fn test_health_probe_never_clears_bypass() {
let state = WorkerState::new(test_config("flapper"));
state
.enter_bypass(BypassFailureClass::DiskInodePressure)
.await;
for _ in 0..5 {
let effective = state.apply_health_status(WorkerStatus::Healthy).await;
assert_eq!(effective, WorkerStatus::Unreachable);
assert_eq!(state.eligibility().await, EligibilityState::TemporaryBypass);
}
}
#[tokio::test]
async fn test_recover_to_canary_then_promote() {
let state = WorkerState::new(test_config("recoverer"));
state.enter_bypass(BypassFailureClass::CircuitBreaker).await;
state
.recover_to_canary()
.await
.expect("bypass -> canary is legal");
assert!(state.is_canary_pending().await);
assert_eq!(
state.eligibility().await,
EligibilityState::RecoveredPendingCanary
);
assert_eq!(state.status().await, WorkerStatus::Unreachable);
assert!(
!state.reserve_slots(1).await,
"canary-pending worker must not take normal builds"
);
state
.promote_from_canary()
.await
.expect("canary-pending -> healthy is legal");
assert_eq!(state.status().await, WorkerStatus::Healthy);
assert!(state.lifecycle().await.is_schedulable());
assert!(!state.is_canary_pending().await);
assert!(state.reserve_slots(1).await, "rejoined worker accepts work");
}
#[tokio::test]
async fn test_recover_to_canary_illegal_from_healthy() {
let state = WorkerState::new(test_config("healthy"));
let err = state.recover_to_canary().await.unwrap_err();
assert_eq!(err.from, EligibilityState::Healthy);
assert_eq!(err.to, EligibilityState::RecoveredPendingCanary);
}
#[tokio::test]
async fn test_enable_clears_bypass() {
let state = WorkerState::new(test_config("reenabled"));
state
.enter_bypass(BypassFailureClass::RuntimeToolchain)
.await;
assert_eq!(state.status().await, WorkerStatus::Unreachable);
state.enable().await;
assert_eq!(state.status().await, WorkerStatus::Healthy);
assert!(state.lifecycle().await.is_schedulable());
assert_eq!(state.lifecycle().await.bypass_cause, None);
}
#[tokio::test]
async fn test_pool_healthy_workers_excludes_bypassed() {
let pool = WorkerPool::new();
pool.add_worker(test_config("healthy")).await;
pool.add_worker(test_config("bypassed")).await;
pool.add_worker(test_config("canary")).await;
pool.get(&WorkerId::new("bypassed"))
.await
.unwrap()
.enter_bypass(BypassFailureClass::Ssh)
.await;
let canary = pool.get(&WorkerId::new("canary")).await.unwrap();
canary.enter_bypass(BypassFailureClass::Ssh).await;
canary.recover_to_canary().await.unwrap();
let healthy = pool.healthy_workers().await;
assert_eq!(healthy.len(), 1);
assert_eq!(healthy[0].config.read().await.id, WorkerId::new("healthy"));
}
#[tokio::test]
async fn test_disable_enable_cycle() {
let state = WorkerState::new(test_config("test"));
state.disable(Some("Maintenance".to_string())).await;
assert!(state.is_disabled().await);
assert_eq!(state.status().await, WorkerStatus::Disabled);
assert_eq!(
state.disabled_reason().await,
Some("Maintenance".to_string())
);
assert!(state.disabled_at().is_some());
state.enable().await;
assert!(!state.is_disabled().await);
assert_eq!(state.status().await, WorkerStatus::Healthy);
assert!(state.disabled_reason().await.is_none());
assert!(state.disabled_at().is_none());
}
#[tokio::test]
async fn test_disable_without_reason() {
let state = WorkerState::new(test_config("test"));
state.disable(None).await;
assert!(state.is_disabled().await);
assert!(state.disabled_reason().await.is_none());
assert!(state.disabled_at().is_some());
}
#[tokio::test]
async fn test_drain_state() {
let state = WorkerState::new(test_config("test"));
assert!(state.reserve_slots(1).await);
state.drain().await;
assert!(state.is_draining().await);
assert_eq!(state.status().await, WorkerStatus::Draining);
assert!(!state.is_disabled().await);
state.enable().await;
assert!(!state.is_draining().await);
assert_eq!(state.status().await, WorkerStatus::Healthy);
}
#[tokio::test]
async fn test_drained_state() {
let state = WorkerState::new(test_config("test"));
state.drain().await;
assert!(!state.is_draining().await);
assert!(state.is_drained().await);
assert_eq!(state.status().await, WorkerStatus::Drained);
state.enable().await;
assert!(!state.is_drained().await);
assert_eq!(state.status().await, WorkerStatus::Healthy);
}
#[tokio::test]
async fn test_drain_complete_with_active_jobs() {
let state = WorkerState::new(test_config("test"));
assert!(state.reserve_slots(1).await);
assert_eq!(state.used_slots(), 1);
state.drain().await;
assert!(state.is_draining().await);
state.check_drain_complete().await;
assert!(state.is_draining().await);
assert!(!state.is_drained().await);
state.release_slots(1).await;
assert_eq!(state.used_slots(), 0);
assert!(!state.is_draining().await);
assert!(state.is_drained().await);
assert_eq!(state.status().await, WorkerStatus::Drained);
}
#[tokio::test]
async fn test_drain_then_disable_disables_after_active_jobs_finish() {
let state = WorkerState::new(test_config("test"));
assert!(state.reserve_slots(1).await);
state
.drain_then_disable(Some("maintenance".to_string()))
.await;
assert_eq!(state.status().await, WorkerStatus::Draining);
assert!(state.disabled_reason().await.is_none());
assert!(state.disabled_at().is_none());
state.release_slots(1).await;
assert_eq!(state.status().await, WorkerStatus::Disabled);
assert_eq!(
state.disabled_reason().await,
Some("maintenance".to_string())
);
assert!(state.disabled_at().is_some());
assert!(!state.remove_after_drain_pending().await);
}
#[tokio::test]
async fn test_cached_projects() {
let state = WorkerState::new(test_config("test"));
assert!(!state.has_cached_project("project-a").await);
state.add_cached_project("project-a".to_string()).await;
assert!(state.has_cached_project("project-a").await);
assert!(!state.has_cached_project("project-b").await);
state.add_cached_project("project-a".to_string()).await;
let projects = state.cached_projects.read().await;
assert_eq!(projects.len(), 1);
drop(projects);
state.add_cached_project("project-b".to_string()).await;
assert!(state.has_cached_project("project-b").await);
let projects = state.cached_projects.read().await;
assert_eq!(projects.len(), 2);
}
#[tokio::test]
async fn test_speed_score() {
let state = WorkerState::new(test_config("test"));
assert_eq!(state.get_speed_score(), 50.0);
state.set_speed_score(85.5);
assert_eq!(state.get_speed_score(), 85.5);
state.set_speed_score(0.0);
assert_eq!(state.get_speed_score(), 0.0);
state.set_speed_score(100.0);
assert_eq!(state.get_speed_score(), 100.0);
}
#[tokio::test]
async fn test_latency_tracking() {
let state = WorkerState::new(test_config("test"));
assert!(state.last_latency_ms().is_none());
state.set_last_latency_ms(Some(42));
assert_eq!(state.last_latency_ms(), Some(42));
state.set_last_latency_ms(Some(100));
assert_eq!(state.last_latency_ms(), Some(100));
state.set_last_latency_ms(None);
assert!(state.last_latency_ms().is_none());
}
#[tokio::test]
async fn test_error_tracking() {
let state = WorkerState::new(test_config("test"));
assert!(state.last_error().await.is_none());
state.set_error("Connection refused".to_string()).await;
assert_eq!(
state.last_error().await,
Some("Connection refused".to_string())
);
state.record_failure(Some("Timeout".to_string())).await;
assert_eq!(state.last_error().await, Some("Timeout".to_string()));
state.record_failure(None).await;
assert_eq!(state.last_error().await, Some("Timeout".to_string()));
}
#[tokio::test]
async fn test_circuit_breaker_basic() {
let state = WorkerState::new(test_config("test"));
assert_eq!(state.circuit_state().await, Some(CircuitState::Closed));
state.record_success().await;
assert_eq!(state.circuit_state().await, Some(CircuitState::Closed));
state.open_circuit().await;
assert_eq!(state.circuit_state().await, Some(CircuitState::Open));
state.half_open_circuit().await;
assert_eq!(state.circuit_state().await, Some(CircuitState::HalfOpen));
state.set_error("Test error".to_string()).await;
state.close_circuit().await;
assert_eq!(state.circuit_state().await, Some(CircuitState::Closed));
assert!(state.last_error().await.is_none());
}
#[tokio::test]
async fn test_capabilities() {
let state = WorkerState::new(test_config("test"));
assert!(!state.has_bun().await);
assert!(!state.has_node().await);
assert!(!state.has_rust().await);
assert!(!state.has_nix().await);
let mut caps = WorkerCapabilities::new();
caps.rustc_version = Some("1.87.0-nightly".to_string());
caps.disk_free_gb = Some(4.0);
caps.disk_total_gb = Some(120.0);
state.set_capabilities(caps).await;
assert!(state.has_rust().await);
assert!(!state.has_bun().await);
assert!(!state.has_node().await);
let pressure = state.pressure_assessment().await;
assert_eq!(
pressure.state,
crate::disk_pressure::PressureState::Critical
);
assert_eq!(
pressure.reason_code,
"disk_critical_without_fresh_telemetry"
);
let mut caps = WorkerCapabilities::new();
caps.rustc_version = Some("1.87.0".to_string());
caps.bun_version = Some("1.2.0".to_string());
caps.node_version = Some("22.0.0".to_string());
state.set_capabilities(caps.clone()).await;
assert!(state.has_rust().await);
assert!(state.has_bun().await);
assert!(state.has_node().await);
let retrieved = state.capabilities().await;
assert_eq!(retrieved.rustc_version, Some("1.87.0".to_string()));
assert_eq!(retrieved.bun_version, Some("1.2.0".to_string()));
}
#[tokio::test]
async fn test_capability_refresh_preserves_fresh_pressure_telemetry() {
let state = WorkerState::new(test_config("test"));
state
.set_pressure_assessment(crate::disk_pressure::PressureAssessment {
state: crate::disk_pressure::PressureState::Healthy,
confidence: crate::disk_pressure::PressureConfidence::High,
reason_code: "all_pressure_rules_within_threshold".to_string(),
policy_rule: "fresh_telemetry".to_string(),
disk_free_gb: Some(50.0),
disk_total_gb: Some(100.0),
disk_free_ratio: Some(0.5),
disk_io_util_pct: Some(0.0),
memory_pressure: Some(10.0),
telemetry_age_secs: Some(10),
telemetry_fresh: true,
evaluated_at_unix_ms: current_unix_ms(),
})
.await;
let mut caps = WorkerCapabilities::new();
caps.rustc_version = Some("1.87.0-nightly".to_string());
caps.disk_free_gb = Some(90.0);
caps.disk_total_gb = Some(120.0);
state.set_capabilities(caps).await;
let pressure = state.pressure_assessment().await;
assert_eq!(pressure.state, crate::disk_pressure::PressureState::Healthy);
assert_eq!(pressure.reason_code, "all_pressure_rules_within_threshold");
assert_eq!(pressure.disk_free_gb, Some(90.0));
assert_eq!(pressure.disk_total_gb, Some(120.0));
assert_eq!(pressure.disk_free_ratio, Some(0.75));
assert_eq!(pressure.disk_io_util_pct, Some(0.0));
assert_eq!(pressure.memory_pressure, Some(10.0));
assert!(pressure.telemetry_fresh);
assert!(pressure.telemetry_age_secs.unwrap_or(u64::MAX) <= 11);
}
#[tokio::test]
async fn test_capability_refresh_critical_disk_overrides_fresh_pressure() {
let state = WorkerState::new(test_config("test"));
state
.set_pressure_assessment(crate::disk_pressure::PressureAssessment {
state: crate::disk_pressure::PressureState::Healthy,
confidence: crate::disk_pressure::PressureConfidence::High,
reason_code: "all_pressure_rules_within_threshold".to_string(),
policy_rule: "fresh_telemetry".to_string(),
disk_free_gb: Some(50.0),
disk_total_gb: Some(100.0),
disk_free_ratio: Some(0.5),
disk_io_util_pct: Some(0.0),
memory_pressure: Some(10.0),
telemetry_age_secs: Some(10),
telemetry_fresh: true,
evaluated_at_unix_ms: current_unix_ms(),
})
.await;
let mut caps = WorkerCapabilities::new();
caps.rustc_version = Some("1.87.0-nightly".to_string());
caps.disk_free_gb = Some(2.0);
caps.disk_total_gb = Some(120.0);
state.set_capabilities(caps).await;
let pressure = state.pressure_assessment().await;
assert_eq!(
pressure.state,
crate::disk_pressure::PressureState::Critical
);
assert_eq!(
pressure.reason_code,
"disk_critical_without_fresh_telemetry"
);
assert_eq!(pressure.disk_free_gb, Some(2.0));
assert!(!pressure.telemetry_fresh);
}
#[tokio::test]
async fn test_toolchain_preflight_cache_records_fail_closed_reason() {
let state = WorkerState::new(test_config("test"));
state
.record_toolchain_preflight(
"nightly-2026-04-30".to_string(),
false,
Some("cargo binary not applicable".to_string()),
)
.await;
let cached = state
.toolchain_preflight_status("nightly-2026-04-30")
.await
.expect("toolchain preflight verdict should be cached");
assert!(!cached.usable);
assert_eq!(
cached.reason.as_deref(),
Some("cargo binary not applicable")
);
assert!(cached.is_fresh(Duration::from_secs(60)));
}
#[tokio::test]
async fn test_update_config() {
let state = WorkerState::new(test_config("test"));
{
let config = state.config.read().await;
assert_eq!(config.total_slots, 8);
assert_eq!(config.priority, 100);
}
let mut new_config = test_config("test");
new_config.total_slots = 16;
new_config.priority = 200;
state.update_config(new_config).await;
let config = state.config.read().await;
assert_eq!(config.total_slots, 16);
assert_eq!(config.priority, 200);
}
#[tokio::test]
async fn test_update_config_cancels_pending_removal_drain() {
let state = WorkerState::new(test_config("test"));
assert!(state.reserve_slots(1).await);
state.drain_for_removal().await;
assert_eq!(state.status().await, WorkerStatus::Draining);
assert!(state.remove_after_drain_pending().await);
let mut new_config = test_config("test");
new_config.priority = 250;
state.update_config(new_config).await;
assert_eq!(state.status().await, WorkerStatus::Healthy);
assert!(!state.remove_after_drain_pending().await);
state.release_slots(1).await;
assert_eq!(state.status().await, WorkerStatus::Healthy);
}
#[tokio::test]
async fn test_pool_new_empty() {
let pool = WorkerPool::new();
assert!(pool.is_empty());
assert_eq!(pool.len(), 0);
}
#[tokio::test]
async fn test_pool_add_and_get() {
let pool = WorkerPool::new();
pool.add_worker(test_config("worker-1")).await;
assert_eq!(pool.len(), 1);
assert!(!pool.is_empty());
let worker = pool.get(&WorkerId::new("worker-1")).await;
assert!(worker.is_some());
let worker = worker.unwrap();
assert_eq!(worker.config.read().await.total_slots, 8);
assert!(pool.get(&WorkerId::new("worker-2")).await.is_none());
}
#[tokio::test]
async fn test_pool_add_duplicate_updates() {
let pool = WorkerPool::new();
pool.add_worker(test_config("worker-1")).await;
assert_eq!(pool.len(), 1);
let mut updated_config = test_config("worker-1");
updated_config.total_slots = 16;
pool.add_worker(updated_config).await;
assert_eq!(pool.len(), 1);
let worker = pool.get(&WorkerId::new("worker-1")).await.unwrap();
assert_eq!(worker.config.read().await.total_slots, 16);
}
#[tokio::test]
async fn test_pool_remove_worker() {
let pool = WorkerPool::new();
pool.add_worker(test_config("worker-1")).await;
pool.add_worker(test_config("worker-2")).await;
assert_eq!(pool.len(), 2);
assert!(pool.remove_worker(&WorkerId::new("worker-1")).await);
assert_eq!(pool.len(), 1);
assert!(pool.get(&WorkerId::new("worker-1")).await.is_none());
assert!(pool.get(&WorkerId::new("worker-2")).await.is_some());
assert!(!pool.remove_worker(&WorkerId::new("worker-1")).await);
assert_eq!(pool.len(), 1);
}
#[tokio::test]
async fn test_pool_all_workers() {
let pool = WorkerPool::new();
pool.add_worker(test_config("worker-1")).await;
pool.add_worker(test_config("worker-2")).await;
pool.get(&WorkerId::new("worker-1"))
.await
.unwrap()
.disable(None)
.await;
let all = pool.all_workers().await;
assert_eq!(all.len(), 2);
}
#[tokio::test]
async fn test_pool_healthy_workers() {
let pool = WorkerPool::new();
pool.add_worker(test_config("healthy")).await;
pool.add_worker(test_config("degraded")).await;
pool.add_worker(test_config("unreachable")).await;
pool.add_worker(test_config("disabled")).await;
pool.add_worker(test_config("draining")).await;
pool.get(&WorkerId::new("degraded"))
.await
.unwrap()
.set_status(WorkerStatus::Degraded)
.await;
pool.get(&WorkerId::new("unreachable"))
.await
.unwrap()
.set_status(WorkerStatus::Unreachable)
.await;
pool.get(&WorkerId::new("disabled"))
.await
.unwrap()
.disable(None)
.await;
pool.get(&WorkerId::new("draining"))
.await
.unwrap()
.drain()
.await;
let healthy = pool.healthy_workers().await;
assert_eq!(healthy.len(), 2);
let mut ids = Vec::new();
for w in &healthy {
ids.push(w.config.read().await.id.clone());
}
assert!(ids.contains(&WorkerId::new("healthy")));
assert!(ids.contains(&WorkerId::new("degraded")));
}
#[tokio::test]
async fn test_pool_set_status() {
let pool = WorkerPool::new();
pool.add_worker(test_config("worker-1")).await;
pool.set_status(&WorkerId::new("worker-1"), WorkerStatus::Unreachable)
.await;
let worker = pool.get(&WorkerId::new("worker-1")).await.unwrap();
assert_eq!(worker.status().await, WorkerStatus::Unreachable);
pool.set_status(&WorkerId::new("nonexistent"), WorkerStatus::Healthy)
.await;
}
#[tokio::test]
async fn test_pool_release_slots() {
let pool = WorkerPool::new();
pool.add_worker(test_config("worker-1")).await;
let worker = pool.get(&WorkerId::new("worker-1")).await.unwrap();
worker.reserve_slots(4).await;
assert_eq!(worker.available_slots().await, 4);
pool.release_slots(&WorkerId::new("worker-1"), 2).await;
assert_eq!(worker.available_slots().await, 6);
pool.release_slots(&WorkerId::new("nonexistent"), 10).await;
}
#[tokio::test]
async fn test_prune_drained() {
let pool = WorkerPool::new();
let active = WorkerState::new(WorkerConfig {
id: WorkerId::new("active"),
host: "localhost".to_string(),
user: "u".to_string(),
identity_file: "i".to_string(),
total_slots: 8,
priority: 100,
tags: vec![],
tools: Vec::new(),
});
pool.add_worker_state(active).await;
let user_drained_empty = WorkerState::new(WorkerConfig {
id: WorkerId::new("user_drained_empty"),
host: "localhost".to_string(),
user: "u".to_string(),
identity_file: "i".to_string(),
total_slots: 8,
priority: 100,
tags: vec![],
tools: Vec::new(),
});
user_drained_empty.drain().await;
pool.add_worker_state(user_drained_empty).await;
let removed_empty = WorkerState::new(WorkerConfig {
id: WorkerId::new("removed_empty"),
host: "localhost".to_string(),
user: "u".to_string(),
identity_file: "i".to_string(),
total_slots: 8,
priority: 100,
tags: vec![],
tools: Vec::new(),
});
removed_empty.drain_for_removal().await;
pool.add_worker_state(removed_empty).await;
let removed_busy = WorkerState::new(WorkerConfig {
id: WorkerId::new("removed_busy"),
host: "localhost".to_string(),
user: "u".to_string(),
identity_file: "i".to_string(),
total_slots: 8,
priority: 100,
tags: vec![],
tools: Vec::new(),
});
assert!(removed_busy.reserve_slots(1).await);
removed_busy.drain_for_removal().await;
pool.add_worker_state(removed_busy).await;
assert_eq!(pool.len(), 4);
let pruned = pool.prune_drained().await;
assert_eq!(pruned, 1);
assert_eq!(pool.len(), 3);
assert!(pool.get(&WorkerId::new("active")).await.is_some());
assert!(
pool.get(&WorkerId::new("user_drained_empty"))
.await
.is_some()
);
assert!(pool.get(&WorkerId::new("removed_busy")).await.is_some());
assert!(pool.get(&WorkerId::new("removed_empty")).await.is_none());
let removed_busy = pool.get(&WorkerId::new("removed_busy")).await.unwrap();
removed_busy.release_slots(1).await;
assert_eq!(pool.prune_drained().await, 1);
assert!(pool.get(&WorkerId::new("removed_busy")).await.is_none());
}
#[tokio::test]
async fn test_prune_drained_empty_pool() {
let pool = WorkerPool::new();
let pruned = pool.prune_drained().await;
assert_eq!(pruned, 0);
}
#[tokio::test]
async fn test_prune_drained_no_draining_workers() {
let pool = WorkerPool::new();
pool.add_worker(test_config("worker-1")).await;
pool.add_worker(test_config("worker-2")).await;
let pruned = pool.prune_drained().await;
assert_eq!(pruned, 0);
assert_eq!(pool.len(), 2);
}
#[tokio::test]
async fn test_pool_default() {
let pool = WorkerPool::default();
assert!(pool.is_empty());
}
}