use std::sync::Arc;
use std::time::Duration;
use chrono::Utc;
use tokio::sync::Mutex;
use tokio::task::JoinHandle;
use tokio::time::interval;
use tracing::{debug, info, warn};
use rch_common::bypass_record::{
BypassRecord, BypassRecordStore, BypassState, classify_disable_reason,
};
use rch_common::bypass_recovery::{
CanaryDecision, CanaryOutcome, ProbeDecision, RecoveryProbe, decide_canary, decide_probe,
};
use rch_common::capability_probe::{
FACT_PREFIX, NamedToolProbe, ProbeSpec, build_capability_probe_script, parse_capability_probe,
remote_worker_binary_path,
};
use rch_common::ssh::{SshClient, SshOptions};
use rch_common::{BypassFailureClass, WorkerConfig, WorkerId};
use rch_telemetry::remediation::{self, BypassTransition, SelfHealingAction, SelfHealingOutcome};
use crate::telemetry::TelemetryStore;
use crate::workers::{AdminIntent, EligibilityState, WorkerPool, WorkerState};
fn now_unix_ms() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| u64::try_from(d.as_millis()).unwrap_or(u64::MAX))
.unwrap_or(0)
}
const BYTES_PER_GB: f64 = 1024.0 * 1024.0 * 1024.0;
#[derive(Debug, Clone, PartialEq)]
pub struct BypassRecoveryConfig {
pub check_interval: Duration,
pub probe_timeout: Duration,
pub min_disk_free_gb: f64,
pub min_disk_inodes: u64,
pub disk_roots: Vec<String>,
pub max_load_per_core: f64,
pub min_protocol: u32,
pub required_targets: Vec<String>,
pub required_toolchains: Vec<String>,
pub telemetry_max_age: Duration,
pub canary_command: String,
}
impl Default for BypassRecoveryConfig {
fn default() -> Self {
Self {
check_interval: Duration::from_secs(30),
probe_timeout: Duration::from_secs(10),
min_disk_free_gb: 5.0,
min_disk_inodes: 10_000,
disk_roots: vec!["/tmp".to_string(), "/tmp/rch".to_string()],
max_load_per_core: 4.0,
min_protocol: 0,
required_targets: Vec::new(),
required_toolchains: Vec::new(),
telemetry_max_age: Duration::from_secs(120),
canary_command: "rustc --version".to_string(),
}
}
}
impl BypassRecoveryConfig {
#[must_use]
pub fn from_remediation(rem: &rch_common::remediation_config::RemediationConfig) -> Self {
let ar = &rem.auto_rejoin;
Self {
check_interval: Duration::from_secs(ar.check_interval_secs),
probe_timeout: Duration::from_secs(ar.probe_timeout_secs),
min_disk_free_gb: ar.min_disk_free_gb,
min_disk_inodes: ar.min_disk_inodes,
disk_roots: ar.disk_roots.clone(),
max_load_per_core: ar.max_load_per_core,
min_protocol: ar.min_protocol,
required_targets: ar.required_targets.clone(),
required_toolchains: ar.required_toolchains.clone(),
telemetry_max_age: Duration::from_secs(rem.telemetry_freshness.max_age_secs),
canary_command: ar.canary_command.clone(),
}
}
}
pub trait RecoveryProber: Send + Sync {
fn probe(
&self,
worker: Arc<WorkerState>,
) -> impl std::future::Future<Output = RecoveryProbe> + Send;
fn canary(
&self,
worker: Arc<WorkerState>,
) -> impl std::future::Future<Output = CanaryOutcome> + Send;
}
pub struct SshRecoveryProber {
telemetry: Arc<TelemetryStore>,
config: BypassRecoveryConfig,
}
impl SshRecoveryProber {
#[must_use]
pub fn new(telemetry: Arc<TelemetryStore>, config: BypassRecoveryConfig) -> Self {
Self { telemetry, config }
}
fn probe_spec(&self, config: &WorkerConfig) -> ProbeSpec {
let declared_os = rch_common::declared_os(&config.tags);
let mut spec = ProbeSpec::new(
config.user.clone(),
remote_worker_binary_path(&config.user, declared_os.as_deref()),
);
spec.disk_roots.clone_from(&self.config.disk_roots);
spec.tools = config
.tools
.iter()
.filter_map(|tool| NamedToolProbe::try_from(tool).ok())
.collect();
spec
}
fn ssh_options(&self) -> SshOptions {
SshOptions {
command_timeout: self.config.probe_timeout,
connect_timeout: self.config.probe_timeout,
..Default::default()
}
}
async fn ssh_run(&self, config: &WorkerConfig, command: &str) -> Option<String> {
let mut client = SshClient::new(config.clone(), self.ssh_options());
if client.connect().await.is_err() {
return None;
}
client.execute(command).await.ok().map(|r| r.stdout)
}
async fn telemetry_fresh(&self, worker_id: &str) -> bool {
match self.telemetry.latest(worker_id) {
Some(sample) => Utc::now()
.signed_duration_since(sample.received_at)
.to_std()
.is_ok_and(|age| age <= self.config.telemetry_max_age),
None => false,
}
}
fn parse_load_per_core(stdout: &str) -> Option<f64> {
let (mut load1, mut nproc): (Option<f64>, Option<f64>) = (None, None);
for line in stdout.lines() {
let Some(kv) = line.trim().strip_prefix(FACT_PREFIX) else {
continue;
};
if let Some(v) = kv.strip_prefix("loadavg1=") {
load1 = v.trim().parse().ok();
} else if let Some(v) = kv.strip_prefix("nproc=") {
nproc = v.trim().parse().ok();
}
}
match (load1, nproc) {
(Some(l), Some(n)) if n > 0.0 => Some(l / n),
_ => None,
}
}
}
fn assess_probe_facts(
facts: &rch_common::capability_probe::ProbedFacts,
load_per_core: Option<f64>,
telemetry_ok: bool,
config: &BypassRecoveryConfig,
) -> RecoveryProbe {
let reachable = facts.os.is_some() || facts.arch.is_some() || facts.probed_user.is_some();
if !reachable {
return RecoveryProbe {
ssh_ok: false,
worker_binary_ok: false,
protocol_ok: false,
toolchain_ok: false,
disk_ok: false,
load_ok: false,
telemetry_ok: false,
};
}
let worker_binary_ok = facts.worker.is_some();
let protocol_ok = facts
.worker
.as_ref()
.is_some_and(|w| w.protocol_version >= config.min_protocol);
let toolchain_ok = facts.rust.rustc_version.is_some()
&& config
.required_targets
.iter()
.all(|t| facts.rust.targets.iter().any(|have| have == t))
&& config.required_toolchains.iter().all(|t| {
facts
.rust
.toolchains
.iter()
.any(|have| have.starts_with(t.as_str()))
});
let min_bytes = (config.min_disk_free_gb * BYTES_PER_GB) as u64;
let requires_inodes = facts.os.as_deref() != Some("windows");
let disk_ok = if facts.disk_roots.is_empty() {
true
} else {
facts.disk_roots.iter().all(|r| {
r.available_bytes >= min_bytes
&& (!requires_inodes || r.available_inodes >= config.min_disk_inodes)
})
};
let load_ok = load_per_core.is_none_or(|lpc| lpc <= config.max_load_per_core);
RecoveryProbe {
ssh_ok: true,
worker_binary_ok,
protocol_ok,
toolchain_ok,
disk_ok,
load_ok,
telemetry_ok,
}
}
fn recovery_telemetry_ok(config: &WorkerConfig, observed_fresh: bool) -> bool {
observed_fresh
|| rch_common::declared_os(&config.tags)
.is_some_and(|os| os.eq_ignore_ascii_case("windows"))
}
impl RecoveryProber for SshRecoveryProber {
async fn probe(&self, worker: Arc<WorkerState>) -> RecoveryProbe {
let config = worker.config.read().await.clone();
let spec = self.probe_spec(&config);
let mut script = build_capability_probe_script(&spec);
script.push_str(
" la=$(awk 'NR==1{print $1}' /proc/loadavg 2>/dev/null) && printf '%sloadavg1=%s\\n' \"$P\" \"$la\"; \
nc=$(nproc 2>/dev/null) && printf '%snproc=%s\\n' \"$P\" \"$nc\"; ",
);
let Some(stdout) = self.ssh_run(&config, &script).await else {
return RecoveryProbe {
ssh_ok: false,
worker_binary_ok: false,
protocol_ok: false,
toolchain_ok: false,
disk_ok: false,
load_ok: false,
telemetry_ok: false,
};
};
let facts = parse_capability_probe(&stdout);
let load_per_core = Self::parse_load_per_core(&stdout);
let telemetry_ok =
recovery_telemetry_ok(&config, self.telemetry_fresh(config.id.as_str()).await);
assess_probe_facts(&facts, load_per_core, telemetry_ok, &self.config)
}
async fn canary(&self, worker: Arc<WorkerState>) -> CanaryOutcome {
let config = worker.config.read().await.clone();
let mut client = SshClient::new(config, self.ssh_options());
if client.connect().await.is_err() {
return CanaryOutcome::Failed;
}
match client.execute(&self.config.canary_command).await {
Ok(result) if result.exit_code == 0 => CanaryOutcome::Passed,
_ => CanaryOutcome::Failed,
}
}
}
pub async fn record_worker_bypass(
store: &Arc<Mutex<BypassRecordStore>>,
worker: &Arc<WorkerState>,
class: BypassFailureClass,
diagnostic: impl Into<String>,
now_ms: u64,
) {
if worker.lifecycle().await.admin == AdminIntent::Disabled {
return;
}
worker.enter_bypass(class).await;
remediation::record_bypass_transition(BypassTransition::Bypassed);
remediation::record_worker_ineligible(class);
let (id, host, user) = {
let c = worker.config.read().await;
(c.id.to_string(), c.host.clone(), c.user.clone())
};
let diagnostic = diagnostic.into();
let mut store = store.lock().await;
let record = if let Some(existing) = store.get(&id) {
let mut rec = existing.clone();
rec.record_failure(now_ms, diagnostic);
rec
} else {
BypassRecord::new(id, host, user, class, now_ms).with_diagnostic(diagnostic)
};
if let Err(e) = store.upsert(record) {
warn!(error = %e, "failed to persist bypass record");
}
}
pub struct BypassRecoveryService<P: RecoveryProber> {
pool: WorkerPool,
store: Arc<Mutex<BypassRecordStore>>,
prober: P,
config: BypassRecoveryConfig,
}
impl<P: RecoveryProber + 'static> BypassRecoveryService<P> {
pub fn new(
pool: WorkerPool,
store: Arc<Mutex<BypassRecordStore>>,
prober: P,
config: BypassRecoveryConfig,
) -> Self {
Self {
pool,
store,
prober,
config,
}
}
pub fn start(self) -> JoinHandle<()> {
tokio::spawn(async move {
self.reconcile_on_start().await;
let mut ticker = interval(self.config.check_interval);
loop {
ticker.tick().await;
let now = now_unix_ms();
self.detect_new_bypasses(now).await;
self.evaluate_once(now).await;
}
})
}
pub async fn detect_new_bypasses(&self, now_ms: u64) {
for worker in self.pool.all_workers().await {
let lifecycle = worker.lifecycle().await;
if lifecycle.admin != AdminIntent::Active
|| lifecycle.eligibility != EligibilityState::Unreachable
{
continue;
}
let id = worker.config.read().await.id.to_string();
if self.store.lock().await.contains(&id) {
continue;
}
let last_error = worker.last_error().await;
let class = last_error
.as_deref()
.and_then(classify_disable_reason)
.unwrap_or(BypassFailureClass::Ssh);
let diagnostic = last_error.unwrap_or_else(|| "worker unreachable".to_string());
info!(worker = %id, ?class, "quarantining unreachable worker into temporary bypass");
record_worker_bypass(&self.store, &worker, class, diagnostic, now_ms).await;
}
}
pub async fn reconcile_on_start(&self) {
let records: Vec<BypassRecord> =
self.store.lock().await.all().into_iter().cloned().collect();
let mut restored = 0_usize;
for record in records {
let Some(worker) = self.pool.get(&WorkerId::new(&record.worker_id)).await else {
continue;
};
if worker.lifecycle().await.admin == AdminIntent::Disabled {
continue;
}
match record.state {
BypassState::TemporaryBypass => worker.enter_bypass(record.failure_class).await,
BypassState::RecoveredPendingCanary => {
worker.enter_bypass(record.failure_class).await;
let _ = worker.recover_to_canary().await;
}
}
restored += 1;
}
if restored > 0 {
info!(
restored,
"reconciled bypassed workers from persisted records"
);
}
}
pub async fn evaluate_once(&self, now_ms: u64) {
let due: Vec<BypassRecord> = {
let store = self.store.lock().await;
store
.all()
.into_iter()
.filter(|r| r.probe_due(now_ms))
.cloned()
.collect()
};
for record in due {
self.evaluate_record(record, now_ms).await;
}
}
async fn evaluate_record(&self, record: BypassRecord, now_ms: u64) {
let worker_id = record.worker_id.clone();
let Some(worker) = self.pool.get(&WorkerId::new(&worker_id)).await else {
warn!(
worker = %worker_id,
event = "reduced_capacity",
reason = "quarantine_record_missing_live_worker",
"retaining durable bypass record until its worker is observable"
);
remediation::record_bypass_transition(BypassTransition::StayBypassed);
return;
};
if worker.lifecycle().await.admin == AdminIntent::Disabled {
debug!(worker = %worker_id, "skipping recovery probe: admin-disabled");
return;
}
let probe = self.prober.probe(worker.clone()).await;
match decide_probe(record, &probe, now_ms) {
ProbeDecision::StayBypassed {
failed_dimension,
record,
} => {
debug!(worker = %worker_id, dimension = %failed_dimension, "recovery probe failed; staying bypassed");
remediation::record_bypass_transition(BypassTransition::StayBypassed);
worker.enter_bypass(record.failure_class).await;
self.persist(*record).await;
}
ProbeDecision::KeepProbing {
consecutive_passes,
required,
record,
} => {
debug!(worker = %worker_id, consecutive_passes, required, "recovery probe passed; keep probing");
remediation::record_bypass_transition(BypassTransition::KeepProbing);
self.persist(*record).await;
}
ProbeDecision::ReadyForCanary { record } => {
info!(worker = %worker_id, "recovery probes passed; running canary");
remediation::record_bypass_transition(BypassTransition::ReadyForCanary);
if worker.recover_to_canary().await.is_err() {
worker.enter_bypass(record.failure_class).await;
let _ = worker.recover_to_canary().await;
}
self.persist((*record).clone()).await;
let outcome = self.prober.canary(worker.clone()).await;
remediation::record_canary(outcome);
match decide_canary(*record, outcome, now_ms) {
CanaryDecision::Rejoin => {
info!(worker = %worker_id, "canary passed; rejoining worker");
remediation::record_bypass_transition(BypassTransition::Rejoin);
remediation::record_self_healing(
SelfHealingAction::WorkerRejoin,
SelfHealingOutcome::Success,
);
self.rejoin(&worker, &worker_id).await;
}
CanaryDecision::Relapse { record } => {
warn!(worker = %worker_id, "canary failed; relapsing into bypass");
remediation::record_bypass_transition(BypassTransition::Relapse);
worker.enter_bypass(record.failure_class).await;
self.persist(*record).await;
}
}
}
ProbeDecision::Rejoin => {
info!(worker = %worker_id, "recovery criteria met (no canary required); rejoining worker");
remediation::record_bypass_transition(BypassTransition::Rejoin);
remediation::record_self_healing(
SelfHealingAction::WorkerRejoin,
SelfHealingOutcome::Success,
);
self.rejoin(&worker, &worker_id).await;
}
}
}
async fn rejoin(&self, worker: &Arc<WorkerState>, worker_id: &str) {
if worker.is_canary_pending().await {
let _ = worker.promote_from_canary().await;
} else {
let _ = worker.recover_to_canary().await;
let _ = worker.promote_from_canary().await;
}
worker.close_circuit().await;
let _ = self.store.lock().await.remove(worker_id);
}
async fn persist(&self, record: BypassRecord) {
if let Err(e) = self.store.lock().await.upsert(record) {
warn!(error = %e, "failed to persist bypass record");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rch_common::bypass_record::AutoRejoinCriteria;
use rch_common::capability_probe::ProbedFacts;
use std::collections::VecDeque;
const GB: u64 = 1024 * 1024 * 1024;
#[test]
fn drift_guard_bypass_recovery_config() {
let from_cfg = BypassRecoveryConfig::from_remediation(
&rch_common::remediation_config::RemediationConfig::default(),
);
assert_eq!(from_cfg, BypassRecoveryConfig::default());
}
fn facts_all_good() -> ProbedFacts {
let out = [
"RCH_FACT os=linux",
"RCH_FACT arch=x86_64",
"RCH_FACT user=ubuntu",
"RCH_FACT rch_wkr_path=/home/ubuntu/.local/bin/rch-wkr",
"RCH_FACT worker_version=rch-wkr 1.0.41",
"RCH_FACT worker_protocol=1",
"RCH_FACT cargo_version=cargo 1.90",
"RCH_FACT toolchain=nightly-2026-05-22-x86_64-unknown-linux-gnu",
"RCH_FACT target=x86_64-unknown-linux-gnu",
"RCH_FACT target=wasm32-unknown-unknown",
"RCH_FACT disk=/tmp;104857600;52428800;1000000",
]
.join("\n");
let facts = parse_capability_probe(&out);
assert_eq!(facts.disk_roots[0].available_bytes, 50 * GB);
facts
}
struct FakeProber {
probes: Mutex<VecDeque<RecoveryProbe>>,
default_probe: RecoveryProbe,
canary: CanaryOutcome,
}
impl FakeProber {
fn new(
probes: Vec<RecoveryProbe>,
default_probe: RecoveryProbe,
canary: CanaryOutcome,
) -> Self {
Self {
probes: Mutex::new(probes.into_iter().collect()),
default_probe,
canary,
}
}
}
impl RecoveryProber for FakeProber {
async fn probe(&self, _worker: Arc<WorkerState>) -> RecoveryProbe {
self.probes
.lock()
.await
.pop_front()
.unwrap_or(self.default_probe)
}
async fn canary(&self, _worker: Arc<WorkerState>) -> CanaryOutcome {
self.canary
}
}
fn worker_config(id: &str) -> WorkerConfig {
WorkerConfig {
id: WorkerId::new(id),
host: "h".to_string(),
user: "u".to_string(),
identity_file: "/dev/null".to_string(),
total_slots: 4,
priority: 100,
tags: vec![],
tools: Vec::new(),
}
}
fn store() -> Arc<Mutex<BypassRecordStore>> {
static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let n = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!("rch_bypass_test_{}_{n}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
Arc::new(Mutex::new(BypassRecordStore::with_path(
dir.join("bypass_records.json"),
)))
}
fn probe_failing(dim: &str) -> RecoveryProbe {
let mut p = RecoveryProbe::all_ok();
match dim {
"ssh" => p.ssh_ok = false,
"worker_binary" => p.worker_binary_ok = false,
"toolchain" => p.toolchain_ok = false,
"disk" => p.disk_ok = false,
"load" => p.load_ok = false,
"telemetry" => p.telemetry_ok = false,
_ => unreachable!(),
}
p
}
async fn pool_with(ids: &[&str]) -> WorkerPool {
let pool = WorkerPool::new();
for id in ids {
pool.add_worker(worker_config(id)).await;
}
pool
}
const T0: u64 = 1_700_000_000_000;
#[tokio::test]
async fn two_healthy_probes_then_passing_canary_rejoins() {
let pool = pool_with(&["css"]).await;
let store = store();
let worker = pool.get(&WorkerId::new("css")).await.unwrap();
record_worker_bypass(
&store,
&worker,
BypassFailureClass::Ssh,
"went unreachable",
T0,
)
.await;
assert!(store.lock().await.contains("css"));
assert!(!worker.lifecycle().await.is_schedulable());
let svc = BypassRecoveryService::new(
pool.clone(),
store.clone(),
FakeProber::new(
vec![RecoveryProbe::all_ok(), RecoveryProbe::all_ok()],
RecoveryProbe::all_ok(),
CanaryOutcome::Passed,
),
BypassRecoveryConfig::default(),
);
svc.evaluate_once(T0 + 60_000).await;
assert!(
store.lock().await.contains("css"),
"one pass must not rejoin"
);
svc.evaluate_once(T0 + 1_000_000).await;
assert!(
!store.lock().await.contains("css"),
"rejoined worker has no record"
);
assert_eq!(worker.status().await, rch_common::WorkerStatus::Healthy);
assert!(worker.lifecycle().await.is_schedulable());
}
#[tokio::test]
async fn one_lucky_ssh_never_rejoins() {
let pool = pool_with(&["css"]).await;
let store = store();
let worker = pool.get(&WorkerId::new("css")).await.unwrap();
record_worker_bypass(&store, &worker, BypassFailureClass::Ssh, "down", T0).await;
let mut lucky = RecoveryProbe::all_ok();
lucky.toolchain_ok = false;
lucky.disk_ok = false;
let svc = BypassRecoveryService::new(
pool.clone(),
store.clone(),
FakeProber::new(vec![], lucky, CanaryOutcome::Passed),
BypassRecoveryConfig::default(),
);
let mut now = T0 + 60_000;
for _ in 0..8 {
svc.evaluate_once(now).await;
now += 2_000_000;
}
assert!(
store.lock().await.contains("css"),
"lucky SSH must never rejoin"
);
assert_eq!(worker.status().await, rch_common::WorkerStatus::Unreachable);
}
#[tokio::test]
async fn flapping_worker_never_reaches_canary() {
let pool = pool_with(&["css"]).await;
let store = store();
let worker = pool.get(&WorkerId::new("css")).await.unwrap();
record_worker_bypass(&store, &worker, BypassFailureClass::Ssh, "down", T0).await;
let probes = vec![
RecoveryProbe::all_ok(),
probe_failing("ssh"),
RecoveryProbe::all_ok(),
probe_failing("disk"),
RecoveryProbe::all_ok(),
probe_failing("telemetry"),
];
let svc = BypassRecoveryService::new(
pool.clone(),
store.clone(),
FakeProber::new(probes, probe_failing("ssh"), CanaryOutcome::Passed),
BypassRecoveryConfig::default(),
);
let mut now = T0 + 60_000;
for _ in 0..6 {
svc.evaluate_once(now).await;
now += 4_000_000;
}
assert!(store.lock().await.contains("css"));
let rec = store.lock().await.get("css").cloned().unwrap();
assert!(
rec.consecutive_passes < 2,
"flapping never accumulates 2 passes"
);
assert_eq!(rec.state, BypassState::TemporaryBypass);
}
#[tokio::test]
async fn stale_telemetry_and_wrong_binary_stay_bypassed() {
for dim in ["telemetry", "worker_binary"] {
let pool = pool_with(&["css"]).await;
let store = store();
let worker = pool.get(&WorkerId::new("css")).await.unwrap();
record_worker_bypass(&store, &worker, BypassFailureClass::Ssh, "down", T0).await;
let svc = BypassRecoveryService::new(
pool.clone(),
store.clone(),
FakeProber::new(vec![], probe_failing(dim), CanaryOutcome::Passed),
BypassRecoveryConfig::default(),
);
svc.evaluate_once(T0 + 60_000).await;
svc.evaluate_once(T0 + 5_000_000).await;
assert!(
store.lock().await.contains("css"),
"{dim} failure must stay bypassed"
);
assert!(!worker.lifecycle().await.is_schedulable());
}
}
#[tokio::test]
async fn failing_canary_relapses_into_bypass() {
let pool = pool_with(&["css"]).await;
let store = store();
let worker = pool.get(&WorkerId::new("css")).await.unwrap();
record_worker_bypass(&store, &worker, BypassFailureClass::Ssh, "down", T0).await;
let svc = BypassRecoveryService::new(
pool.clone(),
store.clone(),
FakeProber::new(
vec![RecoveryProbe::all_ok(), RecoveryProbe::all_ok()],
RecoveryProbe::all_ok(),
CanaryOutcome::Failed,
),
BypassRecoveryConfig::default(),
);
svc.evaluate_once(T0 + 60_000).await;
svc.evaluate_once(T0 + 1_000_000).await;
assert!(
store.lock().await.contains("css"),
"failed canary keeps the record"
);
let rec = store.lock().await.get("css").cloned().unwrap();
assert_eq!(rec.state, BypassState::TemporaryBypass);
assert_eq!(rec.consecutive_passes, 0, "relapse resets the pass streak");
assert!(!worker.lifecycle().await.is_schedulable());
}
#[tokio::test]
async fn admin_disabled_worker_is_never_probed_for_rejoin() {
let pool = pool_with(&["css"]).await;
let store = store();
let worker = pool.get(&WorkerId::new("css")).await.unwrap();
record_worker_bypass(&store, &worker, BypassFailureClass::Ssh, "down", T0).await;
worker.disable(Some("maintenance".to_string())).await;
let svc = BypassRecoveryService::new(
pool.clone(),
store.clone(),
FakeProber::new(vec![], RecoveryProbe::all_ok(), CanaryOutcome::Passed),
BypassRecoveryConfig::default(),
);
svc.evaluate_once(T0 + 60_000).await;
svc.evaluate_once(T0 + 5_000_000).await;
assert!(
store.lock().await.contains("css"),
"record untouched for disabled worker"
);
assert_eq!(worker.status().await, rch_common::WorkerStatus::Disabled);
}
#[tokio::test]
async fn missing_live_worker_retains_durable_quarantine_record() {
let pool = pool_with(&[]).await;
let store = store();
let record = BypassRecord::new("missing", "h", "u", BypassFailureClass::Ssh, T0);
store.lock().await.upsert(record.clone()).unwrap();
let svc = BypassRecoveryService::new(
pool,
store.clone(),
FakeProber::new(vec![], RecoveryProbe::all_ok(), CanaryOutcome::Passed),
BypassRecoveryConfig::default(),
);
svc.evaluate_once(T0 + 60_000).await;
assert_eq!(
store.lock().await.get("missing"),
Some(&record),
"a fresh daemon pool must not erase a durable quarantine record"
);
}
#[tokio::test]
async fn no_canary_required_rejoins_after_passes() {
let pool = pool_with(&["css"]).await;
let store = store();
let worker = pool.get(&WorkerId::new("css")).await.unwrap();
record_worker_bypass(&store, &worker, BypassFailureClass::Ssh, "down", T0).await;
{
let mut s = store.lock().await;
let mut rec = s.get("css").cloned().unwrap();
rec.auto_rejoin = AutoRejoinCriteria {
required_consecutive_passes: 2,
canary_required: false,
};
s.upsert(rec).unwrap();
}
let svc = BypassRecoveryService::new(
pool.clone(),
store.clone(),
FakeProber::new(vec![], RecoveryProbe::all_ok(), CanaryOutcome::Failed),
BypassRecoveryConfig::default(),
);
svc.evaluate_once(T0 + 60_000).await;
svc.evaluate_once(T0 + 1_000_000).await;
assert!(
!store.lock().await.contains("css"),
"no-canary rejoin removes the record"
);
assert!(worker.lifecycle().await.is_schedulable());
}
#[tokio::test]
async fn reconcile_on_start_restores_lifecycle_from_records() {
let pool = pool_with(&["css", "vmi"]).await;
let store = store();
{
let mut s = store.lock().await;
s.upsert(BypassRecord::new(
"css",
"h",
"u",
BypassFailureClass::Ssh,
T0,
))
.unwrap();
let mut canary =
BypassRecord::new("vmi", "h", "u", BypassFailureClass::DiskInodePressure, T0);
canary.state = BypassState::RecoveredPendingCanary;
s.upsert(canary).unwrap();
}
let svc = BypassRecoveryService::new(
pool.clone(),
store.clone(),
FakeProber::new(vec![], RecoveryProbe::all_ok(), CanaryOutcome::Passed),
BypassRecoveryConfig::default(),
);
svc.reconcile_on_start().await;
let css = pool.get(&WorkerId::new("css")).await.unwrap();
let vmi = pool.get(&WorkerId::new("vmi")).await.unwrap();
assert_eq!(
css.eligibility().await,
crate::workers::EligibilityState::TemporaryBypass
);
assert!(
vmi.is_canary_pending().await,
"canary-pending record restores canary-pending lifecycle"
);
}
#[tokio::test]
async fn producer_advances_backoff_on_repeated_failures() {
let pool = pool_with(&["css"]).await;
let store = store();
let worker = pool.get(&WorkerId::new("css")).await.unwrap();
record_worker_bypass(&store, &worker, BypassFailureClass::Ssh, "first", T0).await;
let after_first = store.lock().await.get("css").cloned().unwrap();
record_worker_bypass(
&store,
&worker,
BypassFailureClass::Ssh,
"second",
T0 + 1000,
)
.await;
let after_second = store.lock().await.get("css").cloned().unwrap();
assert_eq!(after_first.consecutive_failures, 1);
assert_eq!(after_second.consecutive_failures, 2);
assert!(after_second.backoff.current_ms > after_first.backoff.current_ms);
assert_eq!(
after_first.first_failure_unix_ms,
after_second.first_failure_unix_ms
);
}
#[tokio::test]
async fn detect_bypasses_only_quarantines_active_unreachable_workers() {
let pool = pool_with(&["healthy", "unreachable", "disabled"]).await;
let store = store();
pool.get(&WorkerId::new("unreachable"))
.await
.unwrap()
.set_status(rch_common::WorkerStatus::Unreachable)
.await;
pool.get(&WorkerId::new("disabled"))
.await
.unwrap()
.disable(Some("operator".to_string()))
.await;
let svc = BypassRecoveryService::new(
pool.clone(),
store.clone(),
FakeProber::new(vec![], RecoveryProbe::all_ok(), CanaryOutcome::Passed),
BypassRecoveryConfig::default(),
);
svc.detect_new_bypasses(T0).await;
let s = store.lock().await;
assert!(
s.contains("unreachable"),
"unreachable worker is quarantined"
);
assert!(!s.contains("healthy"), "healthy worker is left alone");
assert!(
!s.contains("disabled"),
"operator-disabled worker is never auto-bypassed"
);
drop(s);
assert_eq!(
pool.get(&WorkerId::new("unreachable"))
.await
.unwrap()
.eligibility()
.await,
EligibilityState::TemporaryBypass
);
let before = store.lock().await.get("unreachable").cloned().unwrap();
svc.detect_new_bypasses(T0 + 1000).await;
let after = store.lock().await.get("unreachable").cloned().unwrap();
assert_eq!(
before, after,
"re-detecting an already-recorded worker is a no-op"
);
}
#[test]
fn assess_facts_all_good_is_fully_healthy() {
let probe = assess_probe_facts(
&facts_all_good(),
Some(0.5),
true,
&BypassRecoveryConfig::default(),
);
assert!(probe.fully_healthy(), "{:?}", probe.first_failure());
}
#[test]
fn assess_facts_empty_parse_is_unreachable() {
let probe = assess_probe_facts(
&ProbedFacts::default(),
None,
true,
&BypassRecoveryConfig::default(),
);
assert!(!probe.ssh_ok);
assert_eq!(probe.first_failure(), Some("ssh"));
}
#[test]
fn assess_facts_missing_exact_path_binary_fails_binary_and_protocol() {
let mut f = facts_all_good();
f.worker = None; let probe = assess_probe_facts(&f, Some(0.1), true, &BypassRecoveryConfig::default());
assert!(probe.ssh_ok);
assert!(!probe.worker_binary_ok);
assert!(!probe.protocol_ok);
assert!(!probe.fully_healthy());
}
#[test]
fn assess_facts_missing_cargo_or_required_target_fails_toolchain() {
let mut no_cargo = facts_all_good();
no_cargo.rust.rustc_version = None;
assert!(
!assess_probe_facts(&no_cargo, Some(0.1), true, &BypassRecoveryConfig::default())
.toolchain_ok
);
let mut no_wasm = facts_all_good();
no_wasm.rust.targets = vec!["x86_64-unknown-linux-gnu".to_string()];
let needs_wasm = BypassRecoveryConfig {
required_targets: vec!["wasm32-unknown-unknown".to_string()],
..BypassRecoveryConfig::default()
};
assert!(!assess_probe_facts(&no_wasm, Some(0.1), true, &needs_wasm).toolchain_ok);
assert!(assess_probe_facts(&facts_all_good(), Some(0.1), true, &needs_wasm).toolchain_ok);
}
#[test]
fn assess_facts_low_disk_or_inodes_fails_disk() {
let mut low_bytes = facts_all_good();
low_bytes.disk_roots[0].available_bytes = GB; assert!(
!assess_probe_facts(
&low_bytes,
Some(0.1),
true,
&BypassRecoveryConfig::default()
)
.disk_ok
);
let mut low_inodes = facts_all_good();
low_inodes.disk_roots[0].available_inodes = 5; assert!(
!assess_probe_facts(
&low_inodes,
Some(0.1),
true,
&BypassRecoveryConfig::default()
)
.disk_ok
);
}
#[test]
fn assess_facts_no_probed_roots_does_not_trap_disk() {
let mut f = facts_all_good();
f.disk_roots.clear();
assert!(
assess_probe_facts(&f, Some(0.1), true, &BypassRecoveryConfig::default()).disk_ok,
"an unmeasurable disk dimension must not trap a reachable worker"
);
}
#[test]
fn assess_windows_disk_without_unix_inode_counts_preserves_byte_floor() {
let config = BypassRecoveryConfig::default();
for (available_kb, expected) in [("67108864", true), ("1048576", false), ("-", false)] {
let facts = parse_capability_probe(&format!(
"RCH_FACT os=mingw64_nt-10.0-26200\n\
RCH_FACT disk=/tmp;498499580;{available_kb};-\n"
));
assert_eq!(facts.os.as_deref(), Some("windows"));
assert_eq!(facts.disk_roots.len(), 1);
assert_eq!(facts.disk_roots[0].available_inodes, 0);
assert_eq!(
assess_probe_facts(&facts, None, true, &config).disk_ok,
expected,
"available_kb={available_kb}"
);
}
}
#[test]
fn assess_non_windows_disk_still_requires_free_inodes() {
let config = BypassRecoveryConfig::default();
for os in ["linux", "darwin", "unknown"] {
for (inodes, expected) in [("-", false), ("0", false), ("9999", false), ("10000", true)]
{
let facts = parse_capability_probe(&format!(
"RCH_FACT os={os}\n\
RCH_FACT disk=/tmp;498499580;67108864;{inodes}\n"
));
assert_eq!(
assess_probe_facts(&facts, None, true, &config).disk_ok,
expected,
"os={os}, inodes={inodes}"
);
}
}
}
#[test]
fn assess_facts_high_load_fails_but_unknown_load_is_lenient() {
assert!(
!assess_probe_facts(
&facts_all_good(),
Some(99.0),
true,
&BypassRecoveryConfig::default()
)
.load_ok
);
assert!(
assess_probe_facts(
&facts_all_good(),
None,
true,
&BypassRecoveryConfig::default()
)
.load_ok
);
}
#[test]
fn assess_facts_stale_telemetry_fails_telemetry() {
let probe = assess_probe_facts(
&facts_all_good(),
Some(0.1),
false,
&BypassRecoveryConfig::default(),
);
assert!(!probe.telemetry_ok);
assert!(!probe.fully_healthy());
}
#[test]
fn parse_load_per_core_divides_loadavg_by_cores() {
let out = "incidental noise\nRCH_FACT loadavg1=2.0\nRCH_FACT nproc=4\nRCH_FACT os=linux\n";
assert_eq!(SshRecoveryProber::parse_load_per_core(out), Some(0.5));
assert_eq!(
SshRecoveryProber::parse_load_per_core("RCH_FACT loadavg1=2.0\n"),
None
);
assert_eq!(SshRecoveryProber::parse_load_per_core(""), None);
}
#[test]
fn rch_wkr_path_is_absolute_per_user() {
assert_eq!(
remote_worker_binary_path("ubuntu", None),
"/home/ubuntu/.local/bin/rch-wkr"
);
assert_eq!(
remote_worker_binary_path("root", None),
"/root/.local/bin/rch-wkr"
);
assert_eq!(
remote_worker_binary_path("jeffr", Some("windows")),
"/c/Users/jeffr/.local/bin/rch-wkr.exe"
);
}
#[test]
fn recovery_probe_uses_declared_windows_worker_binary_path() {
let prober = SshRecoveryProber::new(
Arc::new(TelemetryStore::new(Duration::from_secs(300), None)),
BypassRecoveryConfig::default(),
);
let mut worker = worker_config("wsurf");
worker.user = "jeffr".to_string();
worker.tags = vec![rch_common::os_tag("windows")];
let spec = prober.probe_spec(&worker);
assert_eq!(spec.rch_wkr_path, "/c/Users/jeffr/.local/bin/rch-wkr.exe");
}
#[test]
fn recovery_telemetry_gap_is_expected_only_for_declared_windows_workers() {
let mut windows = worker_config("wsurf");
windows.tags = vec![rch_common::os_tag("windows")];
assert!(recovery_telemetry_ok(&windows, false));
let linux = worker_config("linux-worker");
assert!(!recovery_telemetry_ok(&linux, false));
assert!(recovery_telemetry_ok(&linux, true));
}
}