#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Estimate {
mean: Option<f64>,
mad: f64,
samples: u64,
}
impl Estimate {
pub const fn new() -> Self {
Estimate {
mean: None,
mad: 0.0,
samples: 0,
}
}
pub fn value(&self) -> Option<f64> {
self.mean
}
pub fn samples(&self) -> u64 {
self.samples
}
pub fn relative_volatility(&self) -> f64 {
match self.mean {
Some(m) if m.abs() > f64::EPSILON => (self.mad / m.abs()).clamp(0.0, 1.0),
_ => 0.0,
}
}
fn next_alpha(&self) -> f64 {
let bootstrap = 1.0 / (self.samples as f64 + 1.0);
let vol = self.relative_volatility();
let volatility_driven = 0.15 + (0.60 - 0.15) * vol;
bootstrap.max(volatility_driven).clamp(0.0, 1.0)
}
pub fn observe(&mut self, sample: f64) {
match self.mean {
None => {
self.mean = Some(sample);
self.mad = 0.0;
}
Some(prev) => {
let alpha = self.next_alpha();
let deviation = (sample - prev).abs();
self.mad = (1.0 - alpha) * self.mad + alpha * deviation;
self.mean = Some((1.0 - alpha) * prev + alpha * sample);
}
}
self.samples = self.samples.saturating_add(1);
}
pub fn seed_prior(&mut self, value: f64) {
if self.samples == 0 {
self.mean = Some(value);
}
}
}
impl Default for Estimate {
fn default() -> Self {
Estimate::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Reliability {
rate: Option<f64>,
samples: u64,
hard_failures: u64,
}
impl Reliability {
pub const fn new() -> Self {
Reliability {
rate: None,
samples: 0,
hard_failures: 0,
}
}
pub fn rate(&self) -> Option<f64> {
self.rate
}
pub fn samples(&self) -> u64 {
self.samples
}
pub fn hard_failures(&self) -> u64 {
self.hard_failures
}
fn alpha(&self) -> f64 {
(1.0 / (self.samples as f64 + 1.0)).max(0.25)
}
pub fn observe(&mut self, ok: bool, hard: bool) {
let target = if ok { 1.0 } else { 0.0 };
let repeats = if !ok && hard { 3 } else { 1 };
for _ in 0..repeats {
match self.rate {
None => self.rate = Some(target),
Some(prev) => {
let a = self.alpha();
self.rate = Some((1.0 - a) * prev + a * target);
}
}
self.samples = self.samples.saturating_add(1);
}
if !ok && hard {
self.hard_failures = self.hard_failures.saturating_add(1);
}
}
pub fn seed_prior(&mut self, value: f64) {
if self.samples == 0 {
self.rate = Some(value.clamp(0.0, 1.0));
}
}
}
impl Default for Reliability {
fn default() -> Self {
Reliability::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PeerQuality {
pub throughput: Estimate,
pub rtt: Estimate,
pub reliability: Reliability,
pub samples: u64,
pub in_flight: u32,
}
impl PeerQuality {
pub const fn cold() -> Self {
PeerQuality {
throughput: Estimate::new(),
rtt: Estimate::new(),
reliability: Reliability::new(),
samples: 0,
in_flight: 0,
}
}
pub fn is_cold(&self) -> bool {
self.samples == 0
}
pub fn confidence(&self) -> f64 {
let n = self.samples as f64;
n / (n + 4.0)
}
pub fn observe_throughput(&mut self, bps: f64) {
self.throughput.observe(bps);
}
pub fn observe_rtt(&mut self, ms: f64) {
self.rtt.observe(ms);
}
pub fn observe_result(&mut self, ok: bool, hard: bool) {
self.reliability.observe(ok, hard);
}
pub fn bump_samples(&mut self) {
self.samples = self.samples.saturating_add(1);
}
}
impl Default for PeerQuality {
fn default() -> Self {
PeerQuality::cold()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn estimate_converges_under_stationary_stream() {
let mut e = Estimate::new();
for _ in 0..50 {
e.observe(1000.0);
}
let v = e.value().unwrap();
assert!((v - 1000.0).abs() < 1.0, "converged to ~1000, got {v}");
assert!(
e.relative_volatility() < 0.01,
"steady stream => low volatility"
);
}
#[test]
fn estimate_responds_to_degradation_within_bounded_samples() {
let mut e = Estimate::new();
for _ in 0..30 {
e.observe(1000.0);
}
for _ in 0..15 {
e.observe(100.0);
}
let v = e.value().unwrap();
assert!(
v < 300.0,
"estimate must follow the drop toward 100 within a bounded window, got {v}"
);
}
#[test]
fn volatile_peer_decays_faster_than_steady_peer() {
let mut steady = Estimate::new();
for _ in 0..30 {
steady.observe(500.0);
}
let mut volatile = Estimate::new();
for i in 0..30 {
volatile.observe(if i % 2 == 0 { 100.0 } else { 900.0 });
}
assert!(
volatile.relative_volatility() > steady.relative_volatility(),
"swinging stream must register higher volatility"
);
let steady_before = steady.value().unwrap();
let volatile_before = volatile.value().unwrap();
steady.observe(2000.0);
volatile.observe(2000.0);
let steady_move = steady.value().unwrap() - steady_before;
let volatile_move = volatile.value().unwrap() - volatile_before;
assert!(
volatile_move > steady_move,
"volatile peer must react more to a new reading (adaptive decay): steady {steady_move}, volatile {volatile_move}"
);
}
#[test]
fn prior_is_overridden_by_first_measurement() {
let mut e = Estimate::new();
e.seed_prior(50.0);
assert_eq!(e.value(), Some(50.0));
assert_eq!(e.samples(), 0, "a prior is not a measured sample");
e.observe(1000.0);
assert_eq!(
e.value(),
Some(1000.0),
"first measurement overrides the prior"
);
assert_eq!(e.samples(), 1);
}
#[test]
fn reliability_falls_on_failure_and_rises_on_recovery() {
let mut r = Reliability::new();
for _ in 0..10 {
r.observe(true, false);
}
assert!(r.rate().unwrap() > 0.9);
for _ in 0..10 {
r.observe(false, false);
}
assert!(
r.rate().unwrap() < 0.3,
"reliability must fall on a failure run"
);
for _ in 0..10 {
r.observe(true, false);
}
assert!(r.rate().unwrap() > 0.7, "reliability must recover");
}
#[test]
fn hard_failure_penalizes_more_than_soft_failure() {
let mut soft = Reliability::new();
let mut hard = Reliability::new();
for _ in 0..5 {
soft.observe(true, false);
hard.observe(true, false);
}
soft.observe(false, false);
hard.observe(false, true);
assert!(
hard.rate().unwrap() < soft.rate().unwrap(),
"a hard (verification) failure must drop reliability more than a soft one"
);
assert_eq!(hard.hard_failures(), 1);
}
#[test]
fn cold_quality_is_uncertain_and_confidence_grows() {
let mut q = PeerQuality::cold();
assert!(q.is_cold());
assert_eq!(q.confidence(), 0.0);
for _ in 0..4 {
q.observe_throughput(500.0);
q.bump_samples();
}
assert!(!q.is_cold());
assert!(
(q.confidence() - 0.5).abs() < 1e-9,
"4 samples => 4/(4+4)=0.5"
);
}
}