use std::sync::Arc;
use std::time::Duration;
use parking_lot::RwLock;
use super::event::{ChainId, NodeId};
pub trait PlacementScorer: Send + Sync + 'static {
fn score(&self, chain: ChainId, node: NodeId) -> Option<f32>;
fn best_alternative(&self, chain: ChainId, exclude: &[NodeId]) -> Option<(NodeId, f32)>;
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct SchedulerConfig {
pub score_floor: f32,
pub hysteresis_gap: f32,
pub cooldown: Duration,
}
impl Default for SchedulerConfig {
fn default() -> Self {
Self {
score_floor: 0.5,
hysteresis_gap: 0.2,
cooldown: Duration::from_secs(5 * 60),
}
}
}
#[derive(Clone, Default)]
pub struct SchedulerRegistry {
inner: Arc<RwLock<Option<Arc<dyn PlacementScorer>>>>,
}
impl SchedulerRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn install(&self, scorer: Arc<dyn PlacementScorer>) -> Option<Arc<dyn PlacementScorer>> {
let mut guard = self.inner.write();
guard.replace(scorer)
}
pub fn current(&self) -> Option<Arc<dyn PlacementScorer>> {
self.inner.read().clone()
}
pub fn has_scorer(&self) -> bool {
self.inner.read().is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
pub(crate) struct FixedScorer {
pub scores: HashMap<(ChainId, NodeId), f32>,
pub alternatives: HashMap<ChainId, (NodeId, f32)>,
}
impl PlacementScorer for FixedScorer {
fn score(&self, chain: ChainId, node: NodeId) -> Option<f32> {
self.scores.get(&(chain, node)).copied()
}
fn best_alternative(&self, chain: ChainId, exclude: &[NodeId]) -> Option<(NodeId, f32)> {
let (n, s) = self.alternatives.get(&chain).copied()?;
if exclude.contains(&n) {
None
} else {
Some((n, s))
}
}
}
#[test]
fn fixed_scorer_returns_table_entries() {
let mut scorer = FixedScorer {
scores: HashMap::new(),
alternatives: HashMap::new(),
};
scorer.scores.insert((1, 100), 0.4);
scorer.alternatives.insert(1, (200, 0.9));
assert_eq!(scorer.score(1, 100), Some(0.4));
assert_eq!(scorer.score(1, 999), None);
assert_eq!(scorer.best_alternative(1, &[]), Some((200, 0.9)));
assert_eq!(scorer.best_alternative(1, &[200]), None);
}
#[test]
fn registry_install_replaces_and_returns_prior() {
let reg = SchedulerRegistry::new();
assert!(!reg.has_scorer());
let s1 = Arc::new(FixedScorer {
scores: HashMap::new(),
alternatives: HashMap::new(),
});
let prior = reg.install(Arc::clone(&s1) as Arc<dyn PlacementScorer>);
assert!(prior.is_none());
assert!(reg.has_scorer());
let s2 = Arc::new(FixedScorer {
scores: HashMap::new(),
alternatives: HashMap::new(),
});
let prior2 = reg.install(s2 as Arc<dyn PlacementScorer>);
assert!(prior2.is_some());
}
#[test]
fn scheduler_config_defaults_match_the_plan() {
let cfg = SchedulerConfig::default();
assert!((cfg.score_floor - 0.5).abs() < 1e-6);
assert!((cfg.hysteresis_gap - 0.2).abs() < 1e-6);
assert_eq!(cfg.cooldown, Duration::from_secs(5 * 60));
}
}