use std::cmp::Ordering;
use rch_common::WorkerId;
use crate::status_types::{DaemonFullStatusResponse, WorkerStatusFromApi};
const WARNING_MEMORY_PRESSURE: f64 = 80.0;
const CRITICAL_MEMORY_PRESSURE: f64 = 92.0;
#[derive(Debug, Clone, PartialEq)]
pub(super) struct WorkerCapacitySnapshot {
pub(super) id: WorkerId,
pub(super) healthy: bool,
pub(super) pressure_critical: bool,
pub(super) free_slots: u32,
pub(super) total_slots: u32,
pub(super) speed_score: f64,
pub(super) memory_pressure: Option<f64>,
}
impl WorkerCapacitySnapshot {
fn from_status(worker: &WorkerStatusFromApi) -> Self {
let healthy = worker.status.eq_ignore_ascii_case("healthy")
&& !worker.circuit_state.eq_ignore_ascii_case("open");
let pressure_critical = worker
.pressure_state
.as_deref()
.is_some_and(|state| state.eq_ignore_ascii_case("critical"));
Self {
id: WorkerId::new(worker.id.clone()),
healthy,
pressure_critical,
free_slots: worker.total_slots.saturating_sub(worker.used_slots),
total_slots: worker.total_slots,
speed_score: worker.speed_score,
memory_pressure: worker.pressure_memory_pressure,
}
}
fn memory_headroom(&self) -> f64 {
match self.memory_pressure {
Some(pressure) => 100.0 - pressure,
None => 50.0,
}
}
fn headroom_bucket(&self) -> u8 {
match self.memory_pressure {
Some(pressure) if pressure >= CRITICAL_MEMORY_PRESSURE => 0,
Some(pressure) if pressure >= WARNING_MEMORY_PRESSURE => 1,
_ => 2,
}
}
}
pub(super) fn build_capacity_snapshots(
status: &DaemonFullStatusResponse,
) -> Vec<WorkerCapacitySnapshot> {
status
.workers
.iter()
.map(WorkerCapacitySnapshot::from_status)
.collect()
}
fn capacity_order(a: &WorkerCapacitySnapshot, b: &WorkerCapacitySnapshot) -> Ordering {
a.headroom_bucket()
.cmp(&b.headroom_bucket())
.then_with(|| a.total_slots.cmp(&b.total_slots))
.then_with(|| {
a.memory_headroom()
.partial_cmp(&b.memory_headroom())
.unwrap_or(Ordering::Equal)
})
.then_with(|| a.free_slots.cmp(&b.free_slots))
.then_with(|| {
a.speed_score
.partial_cmp(&b.speed_score)
.unwrap_or(Ordering::Equal)
})
}
pub(super) fn pick_bigger_worker(
snapshots: &[WorkerCapacitySnapshot],
tried: &[WorkerId],
allowed: &[WorkerId],
) -> Option<WorkerId> {
let mut best: Option<&WorkerCapacitySnapshot> = None;
for candidate in snapshots {
if !candidate.healthy || candidate.pressure_critical {
continue;
}
if tried.iter().any(|id| id == &candidate.id) {
continue;
}
if !allowed.is_empty() && !allowed.iter().any(|id| id == &candidate.id) {
continue;
}
best = match best {
None => Some(candidate),
Some(current) => match capacity_order(candidate, current) {
Ordering::Greater => Some(candidate),
Ordering::Less => Some(current),
Ordering::Equal => {
if candidate.id.as_str() < current.id.as_str() {
Some(candidate)
} else {
Some(current)
}
}
},
};
}
best.map(|snapshot| snapshot.id.clone())
}
#[cfg(test)]
mod tests {
use super::*;
fn snap(
id: &str,
healthy: bool,
total_slots: u32,
free_slots: u32,
memory_pressure: Option<f64>,
) -> WorkerCapacitySnapshot {
WorkerCapacitySnapshot {
id: WorkerId::new(id.to_string()),
healthy,
pressure_critical: false,
free_slots,
total_slots,
speed_score: 50.0,
memory_pressure,
}
}
#[test]
fn picks_the_bigger_machine_when_headroom_is_comparable() {
let snaps = vec![
snap("small", true, 8, 8, Some(20.0)),
snap("big", true, 64, 4, Some(20.0)),
];
let pick = pick_bigger_worker(&snaps, &[], &[]);
assert_eq!(pick, Some(WorkerId::new("big")));
}
#[test]
fn avoids_memory_stressed_worker_even_if_larger() {
let snaps = vec![
snap("big_stressed", true, 128, 100, Some(88.0)),
snap("small_healthy", true, 16, 16, Some(10.0)),
];
let pick = pick_bigger_worker(&snaps, &[], &[]);
assert_eq!(pick, Some(WorkerId::new("small_healthy")));
}
#[test]
fn excludes_tried_and_unhealthy_and_critical_workers() {
let mut critical = snap("critical", true, 256, 256, Some(99.0));
critical.pressure_critical = true;
let snaps = vec![
snap("tried", true, 128, 128, Some(5.0)),
snap("unhealthy", false, 128, 128, Some(5.0)),
critical,
snap("eligible", true, 32, 32, Some(30.0)),
];
let pick = pick_bigger_worker(&snaps, &[WorkerId::new("tried")], &[]);
assert_eq!(pick, Some(WorkerId::new("eligible")));
}
#[test]
fn respects_the_allowed_pin() {
let snaps = vec![
snap("big", true, 128, 128, Some(10.0)),
snap("pinned", true, 16, 16, Some(10.0)),
];
let pick = pick_bigger_worker(&snaps, &[], &[WorkerId::new("pinned")]);
assert_eq!(pick, Some(WorkerId::new("pinned")));
}
#[test]
fn returns_none_when_all_candidates_exhausted() {
let snaps = vec![snap("only", true, 32, 32, Some(10.0))];
let pick = pick_bigger_worker(&snaps, &[WorkerId::new("only")], &[]);
assert_eq!(pick, None);
}
#[test]
fn unknown_pressure_ranks_between_healthy_and_warning() {
let snaps = vec![
snap("warning", true, 32, 32, Some(85.0)),
snap("unknown", true, 32, 32, None),
snap("healthy", true, 32, 32, Some(5.0)),
];
assert_eq!(
pick_bigger_worker(&snaps, &[], &[]),
Some(WorkerId::new("healthy"))
);
assert_eq!(
pick_bigger_worker(&snaps, &[WorkerId::new("healthy")], &[]),
Some(WorkerId::new("unknown"))
);
}
#[test]
fn ties_break_on_ascending_id() {
let snaps = vec![
snap("bbb", true, 32, 32, Some(10.0)),
snap("aaa", true, 32, 32, Some(10.0)),
];
assert_eq!(
pick_bigger_worker(&snaps, &[], &[]),
Some(WorkerId::new("aaa"))
);
}
}