fix(stats): reject stale and discontinuous rolling counter baselines
This commit is contained in:
5 files changed
+177
-34
No files matched your search
@@ -10,12 +10,17 @@ use crate::PacketCounts;
|
||||
/// The directional fields are derived from
|
||||
/// [`PacketCounts::server_packets_received`] for cumulative snapshots. Rolling
|
||||
/// snapshots use the increase in the highest observed count over the retained
|
||||
/// packet-event arrival interval, with the preceding observation as baseline
|
||||
/// packet-event arrival interval within one observation segment. The baseline
|
||||
/// must be that segment's highest count, reported by the preceding packet event
|
||||
/// (zero at the start of the collector's history). They are `None` without a
|
||||
/// known baseline/endpoint or when time filtering leaves an interior packet gap.
|
||||
/// current baseline or known endpoint, or when time filtering leaves an interior
|
||||
/// packet gap. A jump of at least half the
|
||||
/// 32-bit counter range starts a new observation segment: wrap and old replies
|
||||
/// are ambiguous. Windows crossing that discontinuity have no directional loss;
|
||||
/// later windows can use a fresh baseline within the new segment.
|
||||
/// The percentage fields are `0.0` when the corresponding estimate is unavailable.
|
||||
/// These remain estimates: outstanding sends and replies crossing a window
|
||||
/// boundary can produce signed values. [`PacketCounts::server_received_window`]
|
||||
/// These remain estimates: outstanding sends, excess server counts, or extra
|
||||
/// replies can produce signed values. [`PacketCounts::server_received_window`]
|
||||
/// is never used to derive loss.
|
||||
pub struct LossStats {
|
||||
/// Locally inferred total lost packets.
|
||||
|
||||
@@ -18,6 +18,8 @@ type ArrivalKey = (Instant, usize);
|
||||
struct DirectionalPosition {
|
||||
packet_events: u128,
|
||||
server_received: Option<u64>,
|
||||
server_observed_at: u128,
|
||||
counter_reset_at: u128,
|
||||
}
|
||||
|
||||
impl DirectionalPosition {
|
||||
@@ -26,10 +28,21 @@ impl DirectionalPosition {
|
||||
StatsEvent::UniqueReply { sample, .. } => {
|
||||
if let Some(count) = sample.received_count {
|
||||
let count = u64::from(count);
|
||||
self.server_received = Some(
|
||||
self.server_received
|
||||
.map_or(count, |current| current.max(count)),
|
||||
);
|
||||
// A jump across half the 32-bit range can be a wrap or an
|
||||
// old reply from the other side of a wrap. Start a new
|
||||
// observation segment without guessing an epoch. Windows
|
||||
// spanning the discontinuity have no directional estimate.
|
||||
if self
|
||||
.server_received
|
||||
.is_some_and(|current| current.abs_diff(count) >= (1_u64 << 31))
|
||||
{
|
||||
self.server_received = None;
|
||||
self.counter_reset_at = self.packet_events + 1;
|
||||
}
|
||||
if self.server_received.is_none_or(|current| count >= current) {
|
||||
self.server_received = Some(count);
|
||||
self.server_observed_at = self.packet_events + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
StatsEvent::Sent { .. }
|
||||
@@ -161,12 +174,18 @@ fn snapshot_window<'a>(
|
||||
core.apply(event.event.clone());
|
||||
}
|
||||
let server_delta = start.zip(end).and_then(|(start, end)| {
|
||||
if !contiguous {
|
||||
if !contiguous || start.counter_reset_at != end.counter_reset_at {
|
||||
return None;
|
||||
}
|
||||
let baseline = start
|
||||
.server_received
|
||||
.or_else(|| (start.packet_events == 0).then_some(0))?;
|
||||
let baseline = if start.packet_events == 0 {
|
||||
0
|
||||
} else if start.server_observed_at == start.packet_events {
|
||||
start.server_received?
|
||||
} else {
|
||||
// Packet events after the observation but outside the window can
|
||||
// have advanced the server count by an unknown amount.
|
||||
return None;
|
||||
};
|
||||
end.server_received.map(|count| count - baseline)
|
||||
});
|
||||
let mut snapshot = core.snapshot();
|
||||
|
||||
Reference in new issue
Block a user