use std::time::Duration;
use parking_lot::Mutex;
use crate::message::{NetMessage, NetMessageV1};
use crate::node::background_task_monitor::BackgroundTaskMonitor;
const ROLLUP_WINDOW: Duration = Duration::from_secs(60);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum OutboundKind {
Connect,
Put,
Get,
Subscribe,
Update,
InterestSync,
NeighborHosting,
Other,
}
impl OutboundKind {
pub(crate) const ALL: [OutboundKind; 8] = [
OutboundKind::Connect,
OutboundKind::Put,
OutboundKind::Get,
OutboundKind::Subscribe,
OutboundKind::Update,
OutboundKind::InterestSync,
OutboundKind::NeighborHosting,
OutboundKind::Other,
];
const fn index(self) -> usize {
match self {
OutboundKind::Connect => 0,
OutboundKind::Put => 1,
OutboundKind::Get => 2,
OutboundKind::Subscribe => 3,
OutboundKind::Update => 4,
OutboundKind::InterestSync => 5,
OutboundKind::NeighborHosting => 6,
OutboundKind::Other => 7,
}
}
const fn stem(self) -> &'static str {
match self {
OutboundKind::Connect => "connect",
OutboundKind::Put => "put",
OutboundKind::Get => "get",
OutboundKind::Subscribe => "subscribe",
OutboundKind::Update => "update",
OutboundKind::InterestSync => "interest_sync",
OutboundKind::NeighborHosting => "neighbor_hosting",
OutboundKind::Other => "other",
}
}
pub(crate) fn classify(msg: &NetMessage) -> Self {
match msg {
NetMessage::V1(v1) => match v1 {
NetMessageV1::Connect(_) => OutboundKind::Connect,
NetMessageV1::Put(_) => OutboundKind::Put,
NetMessageV1::Get(_) => OutboundKind::Get,
NetMessageV1::Subscribe(_) => OutboundKind::Subscribe,
NetMessageV1::Update(_) => OutboundKind::Update,
NetMessageV1::InterestSync { .. } => OutboundKind::InterestSync,
NetMessageV1::NeighborHosting { .. } => OutboundKind::NeighborHosting,
NetMessageV1::Aborted(_)
| NetMessageV1::ReadyState { .. }
| NetMessageV1::SubscribeHint { .. } => OutboundKind::Other,
},
}
}
}
#[derive(Default)]
struct Window {
msgs: [u64; 8],
bytes: [u64; 8],
max_bytes: [u64; 8],
}
pub(crate) struct OutboundMix {
window: Mutex<Window>,
}
impl OutboundMix {
pub(crate) fn new() -> Self {
Self {
window: Mutex::new(Window::default()),
}
}
pub(crate) fn record_sent(&self, kind: OutboundKind, bytes: usize) {
let b = bytes as u64;
let idx = kind.index();
let mut w = self.window.lock();
w.msgs[idx] = w.msgs[idx].saturating_add(1);
w.bytes[idx] = w.bytes[idx].saturating_add(b);
w.max_bytes[idx] = w.max_bytes[idx].max(b);
}
fn take_window(&self) -> Window {
std::mem::take(&mut *self.window.lock())
}
}
fn rollup_window_secs(elapsed: Duration) -> u64 {
elapsed.as_secs().max(1)
}
fn emit_outbound_mix_rollup(mix: &OutboundMix, local_peer_id: &str, window_secs: u64) {
let w = mix.take_window();
let total_msgs: u64 = w.msgs.iter().sum();
let total_bytes: u64 = w.bytes.iter().sum();
let mut body = serde_json::Map::new();
body.insert("window_secs".into(), window_secs.into());
body.insert("total_msgs".into(), total_msgs.into());
body.insert("total_bytes".into(), total_bytes.into());
for kind in OutboundKind::ALL {
let idx = kind.index();
let stem = kind.stem();
body.insert(format!("{stem}_msgs"), w.msgs[idx].into());
body.insert(format!("{stem}_bytes"), w.bytes[idx].into());
body.insert(format!("{stem}_max_bytes"), w.max_bytes[idx].into());
}
crate::tracing::telemetry::send_standalone_shadow_event_with_peer_id(
"outbound_message_mix",
local_peer_id,
serde_json::Value::Object(body),
);
}
pub(crate) fn spawn_outbound_mix_aggregator(
mix: std::sync::Arc<OutboundMix>,
local_peer_id: String,
monitor: &BackgroundTaskMonitor,
) {
let handle = tokio::spawn(async move {
let mut ticker = tokio::time::interval(ROLLUP_WINDOW);
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
ticker.tick().await; let mut last_rollup = tokio::time::Instant::now();
loop {
ticker.tick().await;
let now = tokio::time::Instant::now();
let elapsed = now.saturating_duration_since(last_rollup);
last_rollup = now;
emit_outbound_mix_rollup(&mix, &local_peer_id, rollup_window_secs(elapsed));
}
});
monitor.register("outbound_message_mix_aggregator", handle);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn take_window_resets_the_window() {
let mix = OutboundMix::new();
mix.record_sent(OutboundKind::InterestSync, 500);
let first = mix.take_window();
assert_eq!(first.msgs[OutboundKind::InterestSync.index()], 1);
assert_eq!(first.bytes[OutboundKind::InterestSync.index()], 500);
let second = mix.take_window();
assert!(
second.msgs.iter().all(|m| *m == 0) && second.bytes.iter().all(|b| *b == 0),
"second take must be empty"
);
}
#[test]
fn arms_partition_recorded_bytes() {
let mix = OutboundMix::new();
mix.record_sent(OutboundKind::Get, 10);
mix.record_sent(OutboundKind::Put, 20);
mix.record_sent(OutboundKind::InterestSync, 30);
mix.record_sent(OutboundKind::Get, 40);
let w = mix.take_window();
assert_eq!(w.bytes.iter().sum::<u64>(), 100);
assert_eq!(w.msgs.iter().sum::<u64>(), 4);
assert_eq!(w.bytes[OutboundKind::Get.index()], 50);
}
#[test]
fn max_bytes_tracks_the_largest_single_message() {
let mix = OutboundMix::new();
mix.record_sent(OutboundKind::Update, 100);
mix.record_sent(OutboundKind::Update, 900);
mix.record_sent(OutboundKind::Update, 50);
let w = mix.take_window();
assert_eq!(w.max_bytes[OutboundKind::Update.index()], 900);
assert_eq!(w.bytes[OutboundKind::Update.index()], 1050);
}
#[test]
fn arms_have_unique_indices_and_stems() {
let mut idxs: Vec<usize> = OutboundKind::ALL.iter().map(|k| k.index()).collect();
idxs.sort_unstable();
idxs.dedup();
assert_eq!(idxs.len(), OutboundKind::ALL.len(), "duplicate arm index");
assert_eq!(
*idxs.last().expect("non-empty"),
OutboundKind::ALL.len() - 1,
"indices must be dense so the fixed-size arrays cover them"
);
let mut stems: Vec<&str> = OutboundKind::ALL.iter().map(|k| k.stem()).collect();
stems.sort_unstable();
stems.dedup();
assert_eq!(stems.len(), OutboundKind::ALL.len(), "duplicate field stem");
}
#[test]
fn rollup_window_secs_is_clamped_to_the_real_elapsed_span() {
assert_eq!(rollup_window_secs(Duration::from_millis(10)), 1);
assert_eq!(rollup_window_secs(Duration::from_secs(60)), 60);
assert_eq!(rollup_window_secs(Duration::from_secs(300)), 300);
}
}