Skip to main content

aria_router_algorithm/
elo.rs

1//! Elo ratings table + selection helpers.
2
3use std::collections::HashMap;
4use std::sync::Mutex;
5
6/// In-process Elo ratings (default 1000). Shared across requests in one serve.
7#[derive(Debug, Default)]
8pub struct EloTable {
9    inner: Mutex<HashMap<String, f32>>,
10}
11
12impl EloTable {
13    pub fn rating(&self, model: &str) -> f32 {
14        self.inner
15            .lock()
16            .unwrap()
17            .get(model)
18            .copied()
19            .unwrap_or(1000.0)
20    }
21
22    pub fn set(&self, model: &str, rating: f32) {
23        self.inner.lock().unwrap().insert(model.to_string(), rating);
24    }
25
26    /// Update ratings after an outcome: winner gains, loser loses (K=24).
27    pub fn update_pair(&self, winner: &str, loser: &str) {
28        let mut g = self.inner.lock().unwrap();
29        let rw = *g.get(winner).unwrap_or(&1000.0);
30        let rl = *g.get(loser).unwrap_or(&1000.0);
31        let ew = 1.0 / (1.0 + 10f32.powf((rl - rw) / 400.0));
32        let el = 1.0 - ew;
33        let k = 24.0;
34        g.insert(winner.to_string(), rw + k * (1.0 - ew));
35        g.insert(loser.to_string(), rl + k * (0.0 - el));
36    }
37
38    /// Prefer lower latency as a soft win signal for `model`.
39    pub fn observe_latency(&self, model: &str, latency_ms: f32, peers: &[String]) {
40        if peers.is_empty() {
41            return;
42        }
43        let mut g = self.inner.lock().unwrap();
44        let r = *g.get(model).unwrap_or(&1000.0);
45        // Faster than 100ms baseline → slight rating bump.
46        let score = if latency_ms < 100.0 {
47            1.0
48        } else if latency_ms < 500.0 {
49            0.5
50        } else {
51            0.0
52        };
53        let expected = 0.5;
54        g.insert(model.to_string(), r + 16.0 * (score - expected));
55        let _ = peers;
56    }
57}
58
59/// Global process table used by `elo` / `ratings` algorithms.
60pub fn global_elo() -> &'static EloTable {
61    use std::sync::OnceLock;
62    static TABLE: OnceLock<EloTable> = OnceLock::new();
63    TABLE.get_or_init(EloTable::default)
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn update_pair_and_latency() {
72        let t = EloTable::default();
73        assert!((t.rating("a") - 1000.0).abs() < 1e-3);
74        t.update_pair("a", "b");
75        assert!(t.rating("a") > t.rating("b"));
76        t.set("c", 1000.0);
77        t.observe_latency("c", 50.0, &["peer".into()]);
78        let fast = t.rating("c");
79        assert!(fast > 1000.0);
80        t.observe_latency("c", 600.0, &["peer".into()]);
81        assert!(t.rating("c") < fast);
82        t.observe_latency("d", 10.0, &[]); // no-op without peers
83        assert!((t.rating("d") - 1000.0).abs() < 1e-3);
84        let _ = global_elo().rating("x");
85    }
86}