use crate::burn_rate::BurnRate;
use crate::window::Window;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ErrorBudget {
total_events: f64,
budget_ratio: f64,
}
impl ErrorBudget {
pub fn new(total_events: f64, budget_ratio: f64) -> Self {
Self {
total_events,
budget_ratio,
}
}
pub fn total_events(&self) -> f64 {
self.total_events
}
pub fn budget_ratio(&self) -> f64 {
self.budget_ratio
}
pub fn allowed_bad_events(&self) -> f64 {
self.total_events * self.budget_ratio
}
pub fn remaining_events(&self, observed_bad: f64) -> f64 {
self.allowed_bad_events() - observed_bad
}
pub fn consumed_ratio(&self, observed_bad: f64) -> f64 {
let allowed = self.allowed_bad_events();
if allowed <= 0.0 {
return f64::INFINITY;
}
observed_bad / allowed
}
pub fn remaining_ratio(&self, observed_bad: f64) -> f64 {
(1.0 - self.consumed_ratio(observed_bad)).max(0.0)
}
pub fn time_to_exhaustion(
&self,
observed_bad: f64,
burn_rate: BurnRate,
period: Window,
) -> Option<std::time::Duration> {
burn_rate.time_to_exhaustion(self.remaining_ratio(observed_bad), period)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn budget_999() -> ErrorBudget {
ErrorBudget::new(1_000_000.0, 0.001)
}
#[test]
fn allowed_bad_events_is_total_times_budget() {
assert!((budget_999().allowed_bad_events() - 1_000.0).abs() < 1e-6);
}
#[test]
fn consumed_and_remaining_are_complementary() {
let b = budget_999();
assert!((b.consumed_ratio(250.0) - 0.25).abs() < 1e-12);
assert!((b.remaining_ratio(250.0) - 0.75).abs() < 1e-12);
}
#[test]
fn overspend_clamps_remaining_to_zero() {
let b = budget_999();
assert_eq!(b.remaining_ratio(5_000.0), 0.0);
assert!(b.remaining_events(5_000.0) < 0.0);
}
#[test]
fn time_to_exhaustion_scales_with_burn_rate() {
let b = budget_999();
let period = Window::days(30);
let ttx = b
.time_to_exhaustion(0.0, BurnRate::new(1.0), period)
.unwrap();
assert_eq!(ttx.as_secs(), period.as_secs());
let fast = b
.time_to_exhaustion(0.0, BurnRate::new(10.0), period)
.unwrap();
assert_eq!(fast.as_secs(), period.as_secs() / 10);
assert!(b
.time_to_exhaustion(0.0, BurnRate::new(0.0), period)
.is_none());
}
}