Remove legacy src/config.rs and migrate all application consumers to the nested Settings model. Baseliner, metrics, pingers, ratecontroller, reflector selector, and main now use Settings. Derived min rates become methods on NetworkSettings rather than stored legacy fields. MeasurementType and ObservabilityProtocol move into the settings module.
447 lines
13 KiB
Rust
447 lines
13 KiB
Rust
// SPDX-FileCopyrightText: 2022-Present Charles Corrigan mailto:chas-iot@runegate.org (github @chas-iot)
|
|
// SPDX-FileCopyrightText: 2022-Present Daniel Lakeland mailto:dlakelan@street-artists.org (github @dlakelan)
|
|
// SPDX-FileCopyrightText: 2022-Present Mark Baker mailto:mark@vpost.net (github @Fail-Safe)
|
|
// SPDX-FileCopyrightText: 2022-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
|
//
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
use crate::SHUTDOWN;
|
|
use crate::metrics::{Metric, MetricsSender};
|
|
use crate::pinger::PingReply;
|
|
use crate::settings::Settings;
|
|
use flume::{Receiver, Sender};
|
|
use log::{debug, info};
|
|
use std::collections::HashMap;
|
|
use std::net::IpAddr;
|
|
use std::sync::atomic::Ordering;
|
|
use std::time::Duration;
|
|
use std::time::Instant;
|
|
|
|
const STALE_AFTER: Duration = Duration::from_secs(30);
|
|
const ANOMALY_THRESHOLD_MS: f64 = 5_000.0;
|
|
const BASELINE_HALF_LIFE_SECS: f64 = 135.0;
|
|
const RECENT_HALF_LIFE_SECS: f64 = 0.4;
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct EwmaStats {
|
|
pub down: f64,
|
|
pub up: f64,
|
|
}
|
|
|
|
impl EwmaStats {
|
|
fn from_reply(reply: &PingReply) -> Self {
|
|
Self {
|
|
down: reply.down_time,
|
|
up: reply.up_time,
|
|
}
|
|
}
|
|
|
|
fn update(&mut self, sample: Self, factor: f64) {
|
|
self.down = self.down * factor + sample.down * (1.0 - factor);
|
|
self.up = self.up * factor + sample.up * (1.0 - factor);
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct ReflectorState {
|
|
pub baseline: EwmaStats,
|
|
pub recent: EwmaStats,
|
|
pub last_receive_at: Instant,
|
|
}
|
|
|
|
impl ReflectorState {
|
|
fn new(sample: EwmaStats, received_at: Instant) -> Self {
|
|
Self {
|
|
baseline: sample,
|
|
recent: sample,
|
|
last_receive_at: received_at,
|
|
}
|
|
}
|
|
|
|
fn reset(&mut self, sample: EwmaStats, received_at: Instant) {
|
|
*self = Self::new(sample, received_at);
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct ControlSnapshot {
|
|
pub generated_at: Instant,
|
|
pub reflectors: HashMap<IpAddr, ReflectorState>,
|
|
}
|
|
|
|
pub struct Baseliner {
|
|
pub settings: Settings,
|
|
pub reselect_trigger: Sender<bool>,
|
|
pub start_time: Instant,
|
|
pub stats_rx: Receiver<PingReply>,
|
|
pub control_tx: Sender<ControlSnapshot>,
|
|
pub selection_tx: Option<Sender<ControlSnapshot>>,
|
|
pub baseline_metrics: MetricsSender,
|
|
pub event_metrics: MetricsSender,
|
|
}
|
|
|
|
fn ewma_factor(tick: f64, dur: f64) -> f64 {
|
|
((0.5_f64).ln() / (dur / tick)).exp()
|
|
}
|
|
|
|
fn update_reflector_state(
|
|
state: &mut ReflectorState,
|
|
sample: EwmaStats,
|
|
received_at: Instant,
|
|
start_time: Instant,
|
|
slow_factor: f64,
|
|
fast_factor: f64,
|
|
) -> bool {
|
|
if received_at.saturating_duration_since(state.last_receive_at) > STALE_AFTER {
|
|
state.reset(sample, received_at);
|
|
}
|
|
|
|
if sample.up > state.baseline.up + ANOMALY_THRESHOLD_MS
|
|
|| sample.down > state.baseline.down + ANOMALY_THRESHOLD_MS
|
|
{
|
|
state.last_receive_at = start_time;
|
|
return true;
|
|
}
|
|
|
|
state.baseline.update(sample, slow_factor);
|
|
state.recent.update(sample, fast_factor);
|
|
|
|
state.baseline.down = state.baseline.down.min(state.recent.down);
|
|
state.baseline.up = state.baseline.up.min(state.recent.up);
|
|
state.last_receive_at = received_at;
|
|
|
|
false
|
|
}
|
|
|
|
fn process_reply(
|
|
reflectors: &mut HashMap<IpAddr, ReflectorState>,
|
|
reply: &PingReply,
|
|
start_time: Instant,
|
|
slow_factor: f64,
|
|
fast_factor: f64,
|
|
) -> (ReflectorState, bool) {
|
|
let sample = EwmaStats::from_reply(reply);
|
|
let state = reflectors
|
|
.entry(reply.reflector)
|
|
.or_insert_with(|| ReflectorState::new(sample, reply.last_receive_time_s));
|
|
let anomaly = update_reflector_state(
|
|
state,
|
|
sample,
|
|
reply.last_receive_time_s,
|
|
start_time,
|
|
slow_factor,
|
|
fast_factor,
|
|
);
|
|
|
|
(*state, anomaly)
|
|
}
|
|
|
|
fn control_snapshot(
|
|
reflectors: &HashMap<IpAddr, ReflectorState>,
|
|
generated_at: Instant,
|
|
) -> ControlSnapshot {
|
|
ControlSnapshot {
|
|
generated_at,
|
|
reflectors: reflectors.clone(),
|
|
}
|
|
}
|
|
|
|
impl Baseliner {
|
|
pub fn run(&self) -> anyhow::Result<()> {
|
|
let mut reflectors = HashMap::<IpAddr, ReflectorState>::new();
|
|
|
|
let slow_factor = ewma_factor(
|
|
self.settings.advanced_settings.tick_interval,
|
|
BASELINE_HALF_LIFE_SECS,
|
|
);
|
|
let fast_factor = ewma_factor(
|
|
self.settings.advanced_settings.tick_interval,
|
|
RECENT_HALF_LIFE_SECS,
|
|
);
|
|
|
|
loop {
|
|
if SHUTDOWN.load(Ordering::Relaxed) {
|
|
info!("Baseliner shutting down");
|
|
return Ok(());
|
|
}
|
|
|
|
let reply = self.stats_rx.recv()?;
|
|
let reflector = reply.reflector;
|
|
let (state, anomaly) = process_reply(
|
|
&mut reflectors,
|
|
&reply,
|
|
self.start_time,
|
|
slow_factor,
|
|
fast_factor,
|
|
);
|
|
|
|
if anomaly {
|
|
info!(
|
|
"Reflector {} has OWD > 5 seconds more than baseline, triggering reselection",
|
|
reflector
|
|
);
|
|
self.event_metrics.send(Metric::Event {
|
|
name: "reselection",
|
|
reason: "anomaly",
|
|
reflector: Some(reflector),
|
|
tags: &[],
|
|
});
|
|
let _ = self.reselect_trigger.try_send(true);
|
|
}
|
|
|
|
self.baseline_metrics.send(Metric::Baseline {
|
|
reflector,
|
|
baseline_up_ewma: state.baseline.up,
|
|
baseline_down_ewma: state.baseline.down,
|
|
recent_up_ewma: state.recent.up,
|
|
recent_down_ewma: state.recent.down,
|
|
});
|
|
|
|
debug!(
|
|
"Reflector {} up baseline = {} down baseline = {}",
|
|
reflector, state.baseline.up, state.baseline.down
|
|
);
|
|
debug!(
|
|
"Reflector {} up recent = {} down recent = {}",
|
|
reflector, state.recent.up, state.recent.down
|
|
);
|
|
|
|
let snapshot = control_snapshot(&reflectors, Instant::now());
|
|
if let Some(selection_tx) = &self.selection_tx {
|
|
let _ = selection_tx.send(snapshot.clone());
|
|
}
|
|
self.control_tx.send(snapshot)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::settings::MeasurementType;
|
|
use std::time::Duration;
|
|
|
|
fn state(
|
|
baseline_down: f64,
|
|
baseline_up: f64,
|
|
recent_down: f64,
|
|
recent_up: f64,
|
|
last_receive_at: Instant,
|
|
) -> ReflectorState {
|
|
ReflectorState {
|
|
baseline: EwmaStats {
|
|
down: baseline_down,
|
|
up: baseline_up,
|
|
},
|
|
recent: EwmaStats {
|
|
down: recent_down,
|
|
up: recent_up,
|
|
},
|
|
last_receive_at,
|
|
}
|
|
}
|
|
|
|
fn sample(down: f64, up: f64) -> EwmaStats {
|
|
EwmaStats { down, up }
|
|
}
|
|
|
|
fn reply(reflector: IpAddr, down_time: f64, up_time: f64, received_at: Instant) -> PingReply {
|
|
PingReply {
|
|
reflector,
|
|
measurement_type: MeasurementType::Icmp,
|
|
seq: 0,
|
|
rtt: down_time + up_time,
|
|
current_time: 0,
|
|
down_time,
|
|
up_time,
|
|
originate_timestamp: 0,
|
|
receive_timestamp: 0,
|
|
transmit_timestamp: 0,
|
|
last_receive_time_s: received_at,
|
|
}
|
|
}
|
|
|
|
fn assert_close(actual: f64, expected: f64) {
|
|
assert!(
|
|
(actual - expected).abs() < 1e-9,
|
|
"expected {expected}, got {actual}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn updates_ewmas() {
|
|
let start = Instant::now();
|
|
let receive_time = start + Duration::from_secs(1);
|
|
let mut state = state(100.0, 100.0, 100.0, 100.0, start);
|
|
let anomaly = update_reflector_state(
|
|
&mut state,
|
|
sample(200.0, 300.0),
|
|
receive_time,
|
|
start,
|
|
0.9,
|
|
0.5,
|
|
);
|
|
|
|
assert!(!anomaly);
|
|
assert_close(state.baseline.down, 110.0);
|
|
assert_close(state.baseline.up, 120.0);
|
|
assert_close(state.recent.down, 150.0);
|
|
assert_close(state.recent.up, 200.0);
|
|
assert_eq!(state.last_receive_at, receive_time);
|
|
}
|
|
|
|
#[test]
|
|
fn keeps_baseline_at_or_below_recent() {
|
|
let start = Instant::now();
|
|
let mut state = state(200.0, 200.0, 100.0, 100.0, start);
|
|
let anomaly = update_reflector_state(
|
|
&mut state,
|
|
sample(100.0, 100.0),
|
|
start + Duration::from_secs(1),
|
|
start,
|
|
0.9,
|
|
0.5,
|
|
);
|
|
|
|
assert!(!anomaly);
|
|
assert_close(state.baseline.down, 100.0);
|
|
assert_close(state.baseline.up, 100.0);
|
|
assert_close(state.recent.down, 100.0);
|
|
assert_close(state.recent.up, 100.0);
|
|
}
|
|
|
|
#[test]
|
|
fn resets_stats_after_a_long_gap() {
|
|
let start = Instant::now();
|
|
let receive_time = start + Duration::from_secs(31);
|
|
let mut state = state(100.0, 200.0, 300.0, 400.0, start);
|
|
let anomaly = update_reflector_state(
|
|
&mut state,
|
|
sample(500.0, 600.0),
|
|
receive_time,
|
|
start,
|
|
0.9,
|
|
0.5,
|
|
);
|
|
|
|
assert!(!anomaly);
|
|
assert_close(state.baseline.down, 500.0);
|
|
assert_close(state.baseline.up, 600.0);
|
|
assert_close(state.recent.down, 500.0);
|
|
assert_close(state.recent.up, 600.0);
|
|
assert_eq!(state.last_receive_at, receive_time);
|
|
}
|
|
|
|
#[test]
|
|
fn does_not_reset_at_exactly_thirty_seconds() {
|
|
let start = Instant::now();
|
|
let receive_time = start + STALE_AFTER;
|
|
let mut state = state(100.0, 200.0, 100.0, 200.0, start);
|
|
let anomaly = update_reflector_state(
|
|
&mut state,
|
|
sample(200.0, 300.0),
|
|
receive_time,
|
|
start,
|
|
0.9,
|
|
0.5,
|
|
);
|
|
|
|
assert!(!anomaly);
|
|
assert_close(state.baseline.down, 110.0);
|
|
assert_close(state.baseline.up, 210.0);
|
|
assert_close(state.recent.down, 150.0);
|
|
assert_close(state.recent.up, 250.0);
|
|
}
|
|
|
|
#[test]
|
|
fn marks_anomalies_for_reselection() {
|
|
let start = Instant::now();
|
|
let mut state = state(100.0, 200.0, 300.0, 400.0, start);
|
|
let anomaly = update_reflector_state(
|
|
&mut state,
|
|
sample(5_101.0, 200.0),
|
|
start + Duration::from_secs(1),
|
|
start,
|
|
0.9,
|
|
0.5,
|
|
);
|
|
|
|
assert!(anomaly);
|
|
assert_close(state.baseline.down, 100.0);
|
|
assert_close(state.baseline.up, 200.0);
|
|
assert_close(state.recent.down, 300.0);
|
|
assert_close(state.recent.up, 400.0);
|
|
assert_eq!(state.last_receive_at, start);
|
|
}
|
|
|
|
#[test]
|
|
fn does_not_mark_exact_anomaly_threshold() {
|
|
let start = Instant::now();
|
|
let receive_time = start + Duration::from_secs(1);
|
|
let mut state = state(100.0, 200.0, 100.0, 200.0, start);
|
|
let anomaly = update_reflector_state(
|
|
&mut state,
|
|
sample(5_100.0, 5_200.0),
|
|
receive_time,
|
|
start,
|
|
0.9,
|
|
0.5,
|
|
);
|
|
|
|
assert!(!anomaly);
|
|
assert_eq!(state.last_receive_at, receive_time);
|
|
}
|
|
|
|
#[test]
|
|
fn processed_reply_updates_combined_reflector_state() {
|
|
let start = Instant::now();
|
|
let received_at = start + Duration::from_secs(1);
|
|
let reflector = "192.0.2.1".parse().unwrap();
|
|
let mut reflectors = HashMap::new();
|
|
reflectors.insert(reflector, state(100.0, 100.0, 100.0, 100.0, start));
|
|
|
|
let (updated, anomaly) = process_reply(
|
|
&mut reflectors,
|
|
&reply(reflector, 200.0, 300.0, received_at),
|
|
start,
|
|
0.9,
|
|
0.5,
|
|
);
|
|
|
|
assert!(!anomaly);
|
|
assert_close(updated.baseline.down, 110.0);
|
|
assert_close(updated.baseline.up, 120.0);
|
|
assert_close(updated.recent.down, 150.0);
|
|
assert_close(updated.recent.up, 200.0);
|
|
assert_eq!(updated.last_receive_at, received_at);
|
|
assert_close(reflectors[&reflector].recent.up, 200.0);
|
|
}
|
|
|
|
#[test]
|
|
fn snapshot_contains_updated_combined_state() {
|
|
let start = Instant::now();
|
|
let received_at = start + Duration::from_secs(1);
|
|
let generated_at = received_at + Duration::from_millis(1);
|
|
let reflector = "192.0.2.1".parse().unwrap();
|
|
let mut reflectors = HashMap::new();
|
|
|
|
let (updated, anomaly) = process_reply(
|
|
&mut reflectors,
|
|
&reply(reflector, 20.0, 30.0, received_at),
|
|
start,
|
|
0.9,
|
|
0.5,
|
|
);
|
|
let snapshot = control_snapshot(&reflectors, generated_at);
|
|
|
|
assert!(!anomaly);
|
|
assert_eq!(snapshot.generated_at, generated_at);
|
|
assert_close(
|
|
snapshot.reflectors[&reflector].baseline.down,
|
|
updated.baseline.down,
|
|
);
|
|
assert_close(snapshot.reflectors[&reflector].recent.up, updated.recent.up);
|
|
assert_eq!(snapshot.reflectors[&reflector].last_receive_at, received_at);
|
|
}
|
|
}
|