use std::collections::HashMap;
const DEFAULT_WINDOW_SIZE: usize = 1000;
const DEFAULT_DEVIATION_THRESHOLD: f64 = 0.10;
pub struct SlaTracker {
window_size: usize,
deviation_threshold: f64,
counters: HashMap<String, OutcomeCounter>,
}
struct OutcomeCounter {
expected_probability: f64,
occurrences: Vec<bool>,
total_evaluations: usize,
}
impl SlaTracker {
pub fn new() -> Self {
SlaTracker {
window_size: Self::env_window_size(),
deviation_threshold: Self::env_deviation_threshold(),
counters: HashMap::new(),
}
}
pub fn with_config(window_size: usize, deviation_threshold: f64) -> Self {
SlaTracker {
window_size,
deviation_threshold,
counters: HashMap::new(),
}
}
pub fn record(
&mut self,
node_id: &str,
outcome: &str,
declared_probability: f64,
occurred: bool,
) -> bool {
let key = format!("{}:{}", node_id, outcome);
let counter = self.counters.entry(key).or_insert_with(|| OutcomeCounter {
expected_probability: declared_probability,
occurrences: Vec::with_capacity(self.window_size),
total_evaluations: 0,
});
counter.expected_probability = declared_probability;
counter.total_evaluations += 1;
if counter.occurrences.len() >= self.window_size {
counter.occurrences.remove(0);
}
counter.occurrences.push(occurred);
if counter.occurrences.len() >= 10 {
let observed = counter
.occurrences
.iter()
.filter(|&&o| o)
.count() as f64
/ counter.occurrences.len() as f64;
let deviation = (observed - counter.expected_probability).abs();
deviation > self.deviation_threshold
} else {
false
}
}
pub fn observed_frequency(&self, node_id: &str, outcome: &str) -> f64 {
let key = format!("{}:{}", node_id, outcome);
self.counters
.get(&key)
.map(|c| {
if c.occurrences.is_empty() {
0.0
} else {
c.occurrences.iter().filter(|&&o| o).count() as f64
/ c.occurrences.len() as f64
}
})
.unwrap_or(0.0)
}
pub fn total_evaluations(&self) -> usize {
self.counters.values().map(|c| c.total_evaluations).sum()
}
fn env_window_size() -> usize {
std::env::var("ETDL_SLA_WINDOW")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(DEFAULT_WINDOW_SIZE)
}
fn env_deviation_threshold() -> f64 {
std::env::var("ETDL_SLA_THRESHOLD")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(DEFAULT_DEVIATION_THRESHOLD)
}
}
impl Default for SlaTracker {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sla_tracking() {
let mut tracker = SlaTracker::new();
for _ in 0..100 {
tracker.record("barrier_1", "SUCCESS", 0.95, true);
}
for _ in 0..100 {
tracker.record("barrier_1", "FAILURE", 0.05, false);
}
let freq = tracker.observed_frequency("barrier_1", "SUCCESS");
assert!((freq - 1.0).abs() < 0.01);
}
#[test]
fn test_sla_anomaly_detection() {
let mut tracker = SlaTracker::with_config(100, 0.10);
for _ in 0..20 {
tracker.record("barrier_1", "SUCCESS", 0.95, true);
}
let mut anomaly_detected = false;
for _ in 0..80 {
let is_anomaly = tracker.record("barrier_1", "SUCCESS", 0.95, false);
if is_anomaly {
anomaly_detected = true;
}
}
assert!(anomaly_detected);
}
}