fix(stats): expire backdated events from rolling time windows

This commit is contained in:
Nils Andreas Svee committed 2026-10-03 22:29:42 +07:00
1 parent 48a50d7f3a
commit edff75e55a
5 files changed
+90 -23

No files matched your search

+3 -1
View File
@@ -58,7 +58,9 @@ pub struct StatsConfig {
///
/// This window has no item cap, even in continuous mode. Its memory depends
/// on event volume inside the interval. Expiry occurs on insertion, anchored
/// at the latest event timestamp, not when a snapshot is read.
/// at the maximum observed normalized event timestamp, not when a snapshot
/// is read. Events exactly at the cutoff are included. Backdated events
/// cannot move the anchor backwards or restore expired history.
pub rolling_time: Option<Duration>,
/// Whether matched late replies contribute measurements.
pub late_replies: LateReplyMode,
+3 -4
View File
@@ -118,10 +118,9 @@ pub(crate) fn normalize_event(event: &ClientEvent) -> Option<StatsEvent> {
// `timeout_at` is anchored to the pre-send probe timestamp (see
// irtt-client's AGENTS.md), so it can precede this same probe's
// own post-send `sent_at` when a probe_timeout is small relative
// to the send-call duration. Rolling-window eviction assumes
// events are pushed in non-decreasing `at()` order, so clamp to
// `sent_at` to preserve that invariant without changing the
// public `timeout_at` value on the event itself.
// to the send-call duration. Clamp to `sent_at` so the loss is
// not earlier than its own send. Delayed timeout discovery can
// still put it before other probes' events.
at: (*timeout_at).max(sent_at.mono),
}),
ClientEvent::Warning { at, .. } => Some(StatsEvent::Warning { at: at.mono }),
+12 -5
View File
@@ -1,4 +1,7 @@
use std::{collections::VecDeque, time::Duration};
use std::{
collections::VecDeque,
time::{Duration, Instant},
};
use measurement_stats::CountWindow;
@@ -11,6 +14,7 @@ pub(crate) struct RollingEvents {
time_limit: Option<Duration>,
count_events: Option<CountWindow<StatsEvent>>,
time_events: Option<VecDeque<StatsEvent>>,
time_anchor: Option<Instant>,
late_replies: LateReplyMode,
}
@@ -20,6 +24,7 @@ impl RollingEvents {
time_limit: config.rolling_time,
count_events: config.rolling_count.map(CountWindow::new),
time_events: config.rolling_time.map(|_| VecDeque::new()),
time_anchor: None,
late_replies: config.late_replies,
}
}
@@ -30,12 +35,14 @@ impl RollingEvents {
}
if let (Some(duration), Some(window)) = (self.time_limit, self.time_events.as_mut()) {
let cutoff = event.at().checked_sub(duration);
let anchor = self.time_anchor.map_or(event.at(), |at| at.max(event.at()));
self.time_anchor = Some(anchor);
let cutoff = anchor.checked_sub(duration);
window.push_back(event);
if let Some(cutoff) = cutoff {
while window.front().is_some_and(|event| event.at() < cutoff) {
window.pop_front();
}
// Delayed timeout discovery can backdate events behind newer
// ones. Filter the whole window, preserving replay order.
window.retain(|event| event.at() >= cutoff);
}
}
}