#![allow(dead_code)]
use crate::consensus::NodeId;
#[derive(Debug, Clone)]
pub struct WorkerCapability {
pub cpu_cores: u32,
pub memory_gb: f32,
pub gpu_vram_gb: f32,
pub network_mbps: u32,
pub tags: Vec<String>,
}
impl WorkerCapability {
#[must_use]
pub fn new(
cpu_cores: u32,
memory_gb: f32,
gpu_vram_gb: f32,
network_mbps: u32,
tags: Vec<String>,
) -> Self {
Self {
cpu_cores,
memory_gb,
gpu_vram_gb,
network_mbps,
tags,
}
}
}
#[derive(Debug, Clone)]
pub struct TaskRequirements {
pub min_cpu_cores: u32,
pub min_memory_gb: f32,
pub requires_gpu: bool,
pub min_gpu_vram_gb: f32,
pub preferred_tags: Vec<String>,
}
impl TaskRequirements {
#[must_use]
pub fn new(
min_cpu_cores: u32,
min_memory_gb: f32,
requires_gpu: bool,
min_gpu_vram_gb: f32,
preferred_tags: Vec<String>,
) -> Self {
Self {
min_cpu_cores,
min_memory_gb,
requires_gpu,
min_gpu_vram_gb,
preferred_tags,
}
}
}
pub struct AffinityScore;
impl AffinityScore {
#[must_use]
pub fn compute(capability: &WorkerCapability, requirements: &TaskRequirements) -> f32 {
if capability.cpu_cores < requirements.min_cpu_cores {
return 0.0;
}
if capability.memory_gb < requirements.min_memory_gb {
return 0.0;
}
if requirements.requires_gpu && capability.gpu_vram_gb < requirements.min_gpu_vram_gb {
return 0.0;
}
let mut score = 0.5_f32;
if requirements.requires_gpu && capability.gpu_vram_gb >= requirements.min_gpu_vram_gb {
score += 0.3;
}
for tag in &requirements.preferred_tags {
if capability.tags.contains(tag) {
score += 0.1;
}
}
score.min(1.0)
}
}
#[derive(Debug, Clone)]
pub struct WorkerStatus {
pub id: NodeId,
pub capability: WorkerCapability,
pub load_pct: f32,
pub task_count: u32,
}
impl WorkerStatus {
#[must_use]
pub fn new(id: NodeId, capability: WorkerCapability, load_pct: f32, task_count: u32) -> Self {
Self {
id,
capability,
load_pct,
task_count,
}
}
}
pub struct WorkloadBalancer;
impl WorkloadBalancer {
#[must_use]
pub fn assign_task(
workers: &[WorkerStatus],
requirements: &TaskRequirements,
) -> Option<NodeId> {
workers
.iter()
.filter_map(|w| {
let affinity = AffinityScore::compute(&w.capability, requirements);
if affinity == 0.0 {
return None;
}
let effective = affinity * (1.0 - w.load_pct.clamp(0.0, 1.0));
Some((w.id, effective))
})
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(id, _)| id)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MigrationReason {
LoadBalance,
NodeFailure,
ResourceExhausted,
UserRequest,
}
#[derive(Debug, Clone)]
pub struct TaskMigration {
pub task_id: String,
pub from_node: NodeId,
pub to_node: NodeId,
pub reason: MigrationReason,
pub data_size_bytes: u64,
}
pub struct MigrationPlanner;
impl MigrationPlanner {
#[must_use]
pub fn plan(workers: &[WorkerStatus], threshold_imbalance: f32) -> Vec<TaskMigration> {
let mut migrations = Vec::new();
let overloaded: Vec<&WorkerStatus> = workers
.iter()
.filter(|w| w.load_pct > threshold_imbalance)
.collect();
let underloaded: Vec<&WorkerStatus> = workers
.iter()
.filter(|w| w.load_pct < 1.0 - threshold_imbalance)
.collect();
for (idx, over) in overloaded.iter().enumerate() {
if let Some(under) = underloaded.get(idx) {
migrations.push(TaskMigration {
task_id: format!("task-from-{}", over.id.inner()),
from_node: over.id,
to_node: under.id,
reason: MigrationReason::LoadBalance,
data_size_bytes: 0,
});
}
}
migrations
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cpu_worker(id: u64, cores: u32, load: f32) -> WorkerStatus {
WorkerStatus::new(
NodeId::new(id),
WorkerCapability::new(cores, 16.0, 0.0, 1000, vec![]),
load,
(load * 10.0) as u32,
)
}
fn gpu_worker(id: u64, vram: f32, load: f32) -> WorkerStatus {
WorkerStatus::new(
NodeId::new(id),
WorkerCapability::new(8, 32.0, vram, 10_000, vec!["gpu".to_string()]),
load,
(load * 10.0) as u32,
)
}
fn basic_requirements() -> TaskRequirements {
TaskRequirements::new(4, 8.0, false, 0.0, vec![])
}
fn gpu_requirements() -> TaskRequirements {
TaskRequirements::new(4, 8.0, true, 8.0, vec!["gpu".to_string()])
}
#[test]
fn test_affinity_meets_minimums() {
let cap = WorkerCapability::new(8, 16.0, 0.0, 1000, vec![]);
let req = basic_requirements();
let score = AffinityScore::compute(&cap, &req);
assert!((score - 0.5).abs() < 1e-5, "Expected 0.5, got {score}");
}
#[test]
fn test_affinity_fails_cpu() {
let cap = WorkerCapability::new(2, 16.0, 0.0, 1000, vec![]);
let req = TaskRequirements::new(4, 8.0, false, 0.0, vec![]);
assert_eq!(AffinityScore::compute(&cap, &req), 0.0);
}
#[test]
fn test_affinity_fails_memory() {
let cap = WorkerCapability::new(8, 4.0, 0.0, 1000, vec![]);
let req = TaskRequirements::new(4, 8.0, false, 0.0, vec![]);
assert_eq!(AffinityScore::compute(&cap, &req), 0.0);
}
#[test]
fn test_affinity_gpu_bonus() {
let cap = WorkerCapability::new(8, 32.0, 16.0, 10_000, vec!["gpu".to_string()]);
let req = gpu_requirements();
let score = AffinityScore::compute(&cap, &req);
assert!((score - 0.9).abs() < 1e-5, "Expected 0.9, got {score}");
}
#[test]
fn test_affinity_tag_bonus() {
let cap = WorkerCapability::new(
8,
16.0,
0.0,
1000,
vec!["av1".to_string(), "fast".to_string()],
);
let req = TaskRequirements::new(
4,
8.0,
false,
0.0,
vec!["av1".to_string(), "fast".to_string()],
);
let score = AffinityScore::compute(&cap, &req);
assert!((score - 0.7).abs() < 1e-5, "Expected 0.7, got {score}");
}
#[test]
fn test_affinity_capped_at_one() {
let tags: Vec<String> = (0..10).map(|i| format!("tag{i}")).collect();
let cap = WorkerCapability::new(8, 16.0, 16.0, 10_000, tags.clone());
let req = TaskRequirements::new(4, 8.0, true, 8.0, tags);
let score = AffinityScore::compute(&cap, &req);
assert!(score <= 1.0, "Score must not exceed 1.0");
}
#[test]
fn test_workload_balancer_selects_best() {
let workers = vec![
cpu_worker(1, 8, 0.9), cpu_worker(2, 8, 0.1), cpu_worker(3, 8, 0.5),
];
let req = basic_requirements();
let assigned = WorkloadBalancer::assign_task(&workers, &req);
assert_eq!(assigned, Some(NodeId::new(2)));
}
#[test]
fn test_workload_balancer_no_capable_worker() {
let workers = vec![WorkerStatus::new(
NodeId::new(1),
WorkerCapability::new(2, 4.0, 0.0, 1000, vec![]),
0.0,
0,
)];
let req = TaskRequirements::new(8, 32.0, false, 0.0, vec![]);
let assigned = WorkloadBalancer::assign_task(&workers, &req);
assert!(assigned.is_none());
}
#[test]
fn test_workload_balancer_gpu_task() {
let workers = vec![cpu_worker(1, 8, 0.1), gpu_worker(2, 16.0, 0.3)];
let req = gpu_requirements();
let assigned = WorkloadBalancer::assign_task(&workers, &req);
assert_eq!(assigned, Some(NodeId::new(2)));
}
#[test]
fn test_migration_planner_suggests_migrations() {
let workers = vec![
cpu_worker(1, 8, 0.9), cpu_worker(2, 8, 0.1), ];
let migrations = MigrationPlanner::plan(&workers, 0.7);
assert_eq!(migrations.len(), 1);
assert_eq!(migrations[0].from_node, NodeId::new(1));
assert_eq!(migrations[0].to_node, NodeId::new(2));
assert_eq!(migrations[0].reason, MigrationReason::LoadBalance);
}
#[test]
fn test_migration_planner_no_migrations_balanced() {
let workers = vec![cpu_worker(1, 8, 0.5), cpu_worker(2, 8, 0.5)];
let migrations = MigrationPlanner::plan(&workers, 0.7);
assert!(migrations.is_empty());
}
}