fix(stats): expire backdated events from rolling time windows
This commit is contained in:
5 files changed
+90
-23
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user