use serde::{Deserialize, Serialize};
use std::fmt;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct NodeId(Uuid);
impl NodeId {
pub fn new() -> Self {
Self(Uuid::new_v4())
}
pub fn from_uuid(uuid: Uuid) -> Self {
Self(uuid)
}
pub fn as_uuid(&self) -> &Uuid {
&self.0
}
}
impl Default for NodeId {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for NodeId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "node-{}", &self.0.to_string()[..8])
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ShardId(u64);
impl ShardId {
pub fn new(id: u64) -> Self {
Self(id)
}
pub fn as_u64(&self) -> u64 {
self.0
}
}
impl fmt::Display for ShardId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "shard-{}", self.0)
}
}
impl From<u64> for ShardId {
fn from(id: u64) -> Self {
Self::new(id)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Shard {
pub id: ShardId,
pub owner: Option<NodeId>,
pub state: ShardState,
pub replicas: u32,
pub metadata: std::collections::HashMap<String, String>,
}
impl Shard {
pub fn new(id: impl Into<ShardId>) -> Self {
Self {
id: id.into(),
owner: None,
state: ShardState::Pending,
replicas: 1,
metadata: std::collections::HashMap::new(),
}
}
pub fn with_owner(mut self, owner: NodeId) -> Self {
self.owner = Some(owner);
self
}
pub fn with_replicas(mut self, replicas: u32) -> Self {
self.replicas = replicas;
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ShardState {
Pending,
Allocating,
Active,
Migrating,
Draining,
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Expert {
pub index: usize,
pub node: NodeId,
pub capacity: u32,
pub load: f64,
pub healthy: bool,
pub gpu: Option<GpuResources>,
pub model_version: Option<String>,
pub metadata: std::collections::HashMap<String, String>,
}
impl Expert {
pub fn new(index: usize, node: NodeId) -> Self {
Self {
index,
node,
capacity: 100,
load: 0.0,
healthy: true,
gpu: None,
model_version: None,
metadata: std::collections::HashMap::new(),
}
}
pub fn with_capacity(mut self, capacity: u32) -> Self {
self.capacity = capacity;
self
}
pub fn with_gpu(mut self, gpu: GpuResources) -> Self {
self.gpu = Some(gpu);
self
}
pub fn with_model_version(mut self, version: impl Into<String>) -> Self {
self.model_version = Some(version.into());
self
}
pub fn available(&self) -> bool {
self.healthy && self.load < 0.95
}
pub fn has_gpu(&self) -> bool {
self.gpu.is_some()
}
pub fn has_gpu_memory(&self, required_mb: u64) -> bool {
self.gpu.as_ref().map(|g| g.available_memory_mb() >= required_mb).unwrap_or(false)
}
pub fn update_load(&mut self, load: f64) {
self.load = load.clamp(0.0, 1.0);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GpuResources {
pub device_id: u32,
pub model: String,
pub memory_mb: u64,
pub memory_used_mb: u64,
pub utilization: f64,
pub compute_capability: Option<f32>,
pub tensor_cores: bool,
}
impl GpuResources {
pub fn new(device_id: u32, model: impl Into<String>, memory_mb: u64) -> Self {
Self {
device_id,
model: model.into(),
memory_mb,
memory_used_mb: 0,
utilization: 0.0,
compute_capability: None,
tensor_cores: false,
}
}
pub fn with_compute_capability(mut self, cc: f32) -> Self {
self.compute_capability = Some(cc);
self
}
pub fn with_tensor_cores(mut self, supported: bool) -> Self {
self.tensor_cores = supported;
self
}
pub fn available_memory_mb(&self) -> u64 {
self.memory_mb.saturating_sub(self.memory_used_mb)
}
pub fn update_memory(&mut self, used_mb: u64) {
self.memory_used_mb = used_mb.min(self.memory_mb);
}
pub fn update_utilization(&mut self, util: f64) {
self.utilization = util.clamp(0.0, 1.0);
}
pub fn available(&self) -> bool {
self.utilization < 0.95
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Region(String);
impl Region {
pub fn new(name: impl Into<String>) -> Self {
Self(name.into())
}
pub fn name(&self) -> &str {
&self.0
}
}
impl fmt::Display for Region {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<&str> for Region {
fn from(s: &str) -> Self {
Self::new(s)
}
}
impl From<String> for Region {
fn from(s: String) -> Self {
Self::new(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_node_id_display() {
let id = NodeId::new();
let display = format!("{}", id);
assert!(display.starts_with("node-"));
}
#[test]
fn test_shard_builder() {
let node = NodeId::new();
let shard = Shard::new(42u64)
.with_owner(node)
.with_replicas(3)
.with_metadata("type", "cache");
assert_eq!(shard.id.as_u64(), 42);
assert_eq!(shard.replicas, 3);
assert_eq!(shard.metadata.get("type"), Some(&"cache".to_string()));
}
#[test]
fn test_expert_availability() {
let mut expert = Expert::new(0, NodeId::new());
assert!(expert.available());
expert.update_load(0.96);
assert!(!expert.available());
expert.update_load(0.5);
expert.healthy = false;
assert!(!expert.available());
}
}