use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, RwLock};
use std::time::Instant;
type Generation = u64;
#[derive(Debug, Clone)]
pub struct ModelVersion {
pub generation: Generation,
pub path: Option<String>,
pub loaded_at: Instant,
pub description: String,
}
impl ModelVersion {
pub fn new(generation: Generation, description: impl Into<String>) -> Self {
Self {
generation,
path: None,
loaded_at: Instant::now(),
description: description.into(),
}
}
pub fn age_seconds(&self) -> f64 {
self.loaded_at.elapsed().as_secs_f64()
}
}
pub struct HotReloadCoordinator {
current_generation: Arc<AtomicU64>,
version_history: Arc<RwLock<Vec<ModelVersion>>>,
max_history: usize,
}
impl HotReloadCoordinator {
pub fn new() -> Self {
Self::with_max_history(32)
}
pub fn with_max_history(max_history: usize) -> Self {
Self {
current_generation: Arc::new(AtomicU64::new(0)),
version_history: Arc::new(RwLock::new(Vec::new())),
max_history,
}
}
pub fn record_reload(
&self,
description: impl Into<String>,
path: Option<String>,
) -> Generation {
let new_gen = self.current_generation.fetch_add(1, Ordering::SeqCst) + 1;
let version = ModelVersion {
generation: new_gen,
path,
loaded_at: Instant::now(),
description: description.into(),
};
let mut history = self
.version_history
.write()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if self.max_history > 0 && history.len() >= self.max_history {
history.remove(0);
}
history.push(version);
new_gen
}
pub fn current_generation(&self) -> Generation {
self.current_generation.load(Ordering::Relaxed)
}
pub fn version_history(&self) -> Vec<ModelVersion> {
let history = self
.version_history
.read()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let mut v: Vec<ModelVersion> = history.clone();
v.reverse();
v
}
pub fn current_version(&self) -> Option<ModelVersion> {
let history = self
.version_history
.read()
.unwrap_or_else(|poisoned| poisoned.into_inner());
history.last().cloned()
}
pub fn reload_count(&self) -> usize {
let history = self
.version_history
.read()
.unwrap_or_else(|poisoned| poisoned.into_inner());
history.len()
}
}
impl Default for HotReloadCoordinator {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct ReloadEvent {
pub old_generation: Generation,
pub new_generation: Generation,
pub description: String,
pub timestamp: Instant,
}
pub struct ReloadLog {
events: Vec<ReloadEvent>,
capacity: usize,
}
impl ReloadLog {
pub fn new(capacity: usize) -> Self {
Self {
events: Vec::new(),
capacity,
}
}
pub fn record(&mut self, old: Generation, new: Generation, description: impl Into<String>) {
if self.capacity > 0 && self.events.len() >= self.capacity {
self.events.remove(0);
}
self.events.push(ReloadEvent {
old_generation: old,
new_generation: new,
description: description.into(),
timestamp: Instant::now(),
});
}
pub fn recent_events(&self, n: usize) -> Vec<&ReloadEvent> {
let start = self.events.len().saturating_sub(n);
self.events[start..].iter().collect()
}
pub fn total_events(&self) -> usize {
self.events.len()
}
pub fn summary(&self) -> String {
format!(
"ReloadLog: {} events (capacity {})",
self.events.len(),
self.capacity,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn coordinator_starts_at_zero() {
let c = HotReloadCoordinator::new();
assert_eq!(c.current_generation(), 0);
}
#[test]
fn coordinator_record_increments() {
let c = HotReloadCoordinator::new();
let g1 = c.record_reload("first", None);
let g2 = c.record_reload("second", None);
assert_eq!(g1, 1);
assert_eq!(g2, 2);
assert_eq!(c.current_generation(), 2);
}
#[test]
fn reload_log_basic() {
let mut log = ReloadLog::new(10);
assert_eq!(log.total_events(), 0);
log.record(0, 1, "initial load");
assert_eq!(log.total_events(), 1);
assert!(!log.summary().is_empty());
}
}