Fix: Reduce internal bus load and TreeGuard ticker latency

This commit is contained in:
Herbert Wolverson committed 2026-03-12 16:05:02 -05:00
1 parent c457eac92f
commit e1cb28c957
12 files changed
+161 -125

No files matched your search

@@ -16,6 +16,10 @@ use tracing::{debug, error, info, warn};
use super::BUS_SOCKET_DIRECTORY;
use super::protocol::{decode_session_cbor, encode_reply_cbor, read_frame, write_frame};
fn dropped_reply_response_count(reply: &BusReply) -> usize {
reply.responses.len()
}
/// Implements a Tokio-friendly server using Unix Sockets and the bus protocol.
/// Requests are handled and then forwarded to the handler.
pub struct UnixSocketServer {}
@@ -111,8 +115,11 @@ impl UnixSocketServer {
if let Some((reply_channel, msg)) = ret {
let mut response = BusReply { responses: Vec::with_capacity(8) };
handle_bus_requests(&[msg], &mut response.responses);
if let Err(e) = reply_channel.send(response) {
warn!("Unable to send response back to client: {:?}", e);
if let Err(reply) = reply_channel.send(response) {
warn!(
dropped_response_count = dropped_reply_response_count(&reply),
"Unable to send response back to client; receiver dropped"
);
}
}
},
@@ -226,3 +233,18 @@ pub enum UnixSocketServerError {
#[error("Unable to write to socket")]
WriteFail,
}
#[cfg(test)]
mod tests {
use super::dropped_reply_response_count;
use crate::{BusReply, BusResponse};
#[test]
fn dropped_reply_summary_only_counts_responses() {
let reply = BusReply {
responses: vec![BusResponse::Ack, BusResponse::Ack, BusResponse::Ack],
};
assert_eq!(dropped_reply_response_count(&reply), 3);
}
}
+8 -3
View File
@@ -11,6 +11,7 @@
use lqos_bakery::BakeryCommands;
use lqos_bus::StormguardDebugEntry;
use lqos_config::NetworkJsonTransport;
use lqos_queue_tracker::QUEUE_STRUCTURE_CHANGED_STORMGUARD;
use parking_lot::Mutex;
use std::time::Duration;
@@ -36,6 +37,7 @@ pub static STORMGUARD_DEBUG: Mutex<Vec<StormguardDebugEntry>> = Mutex::new(Vec::
/// nothing to do.
pub async fn start_stormguard(
bakery: crossbeam_channel::Sender<BakeryCommands>,
network_map_provider: fn() -> Vec<(usize, NetworkJsonTransport)>,
) -> anyhow::Result<()> {
let _ = tokio::time::sleep(Duration::from_secs(1)).await;
@@ -90,9 +92,12 @@ pub async fn start_stormguard(
if let (Some(cfg), Some(tracker)) = (&config, &mut site_state_tracker) {
let (active_ping_sample, active_ping_updated) = active_ping.latest();
// Update all the ring buffers
tracker
.read_new_tick_data(cfg, active_ping_sample, active_ping_updated)
.await;
tracker.read_new_tick_data(
cfg,
active_ping_sample,
active_ping_updated,
network_map_provider(),
);
// Check for state changes
tracker.check_state(cfg);
+34 -43
View File
@@ -21,7 +21,8 @@ use crate::site_state::stormguard_state::StormguardState;
use crate::{MOVING_AVERAGE_BUFFER_SIZE, READING_ACCUMULATOR_SIZE};
use crossbeam_channel::Sender;
use lqos_bakery::BakeryCommands;
use lqos_bus::{BusRequest, BusResponse, StormguardDebugDirection, StormguardDebugEntry, TcHandle};
use lqos_bus::{StormguardDebugDirection, StormguardDebugEntry, TcHandle};
use lqos_config::NetworkJsonTransport;
use lqos_queue_tracker::QUEUE_STRUCTURE;
use std::collections::{HashMap, HashSet};
use std::time::{Duration, Instant};
@@ -185,63 +186,53 @@ impl SiteStateTracker {
}
}
pub async fn read_new_tick_data(
pub fn read_new_tick_data(
&mut self,
config: &StormguardConfig,
active_ping_sample: Option<TimedRtt>,
active_ping_updated: bool,
all_nodes: Vec<(usize, NetworkJsonTransport)>,
) {
let requests = vec![BusRequest::GetFullNetworkMap];
let Ok(responses) = lqos_bus::bus_request(requests).await else {
info!("Failed to get lqosd stats");
return;
};
for site in self.sites.values_mut() {
site.current_throughput = (0.0, 0.0);
site.clear_tick_rtt_state();
}
for response in responses {
let BusResponse::NetworkMap(all_nodes) = response else {
for (_, node_info) in all_nodes {
let Some(target) = self.sites.get_mut(&node_info.name) else {
continue;
};
for (_, node_info) in all_nodes {
let Some(target) = self.sites.get_mut(&node_info.name) else {
continue;
};
// Record throughput (Mbps)
let down_mbps = (node_info.current_throughput.0 as f64 * 8.0) / 1_000_000.0;
let up_mbps = (node_info.current_throughput.1 as f64 * 8.0) / 1_000_000.0;
target.throughput_down.add(down_mbps);
target.throughput_up.add(up_mbps);
target.current_throughput = (down_mbps, up_mbps);
// Record throughput (Mbps)
let down_mbps = (node_info.current_throughput.0 as f64 * 8.0) / 1_000_000.0;
let up_mbps = (node_info.current_throughput.1 as f64 * 8.0) / 1_000_000.0;
target.throughput_down.add(down_mbps);
target.throughput_up.add(up_mbps);
target.current_throughput = (down_mbps, up_mbps);
// Retransmits (as a percentage of TCP packets)
let retransmits_down = if node_info.current_tcp_packets.0 > 0 {
node_info.current_retransmits.0 as f64 / node_info.current_tcp_packets.0 as f64
} else {
0.0
};
let retransmits_up = if node_info.current_tcp_packets.1 > 0 {
node_info.current_retransmits.1 as f64 / node_info.current_tcp_packets.1 as f64
} else {
0.0
};
target.retransmits_down.add(retransmits_down);
target.retransmits_up.add(retransmits_up);
// Retransmits (as a percentage of TCP packets)
let retransmits_down = if node_info.current_tcp_packets.0 > 0 {
node_info.current_retransmits.0 as f64 / node_info.current_tcp_packets.0 as f64
} else {
0.0
};
let retransmits_up = if node_info.current_tcp_packets.1 > 0 {
node_info.current_retransmits.1 as f64 / node_info.current_tcp_packets.1 as f64
} else {
0.0
};
target.retransmits_down.add(retransmits_down);
target.retransmits_up.add(retransmits_up);
// Round-Trip Time
if !node_info.rtts.is_empty() {
let mut my_round_trip_times = node_info.rtts.clone();
my_round_trip_times.sort_by(|a, b| a.total_cmp(b));
let samples = my_round_trip_times.len();
let mut idx = ((samples as f32) * 0.9).floor() as usize;
idx = idx.min(samples.saturating_sub(1));
let p90 = my_round_trip_times[idx] as f64;
target.record_passive_rtt_sample(p90);
}
// Round-Trip Time
if !node_info.rtts.is_empty() {
let mut my_round_trip_times = node_info.rtts.clone();
my_round_trip_times.sort_by(|a, b| a.total_cmp(b));
let samples = my_round_trip_times.len();
let mut idx = ((samples as f32) * 0.9).floor() as usize;
idx = idx.min(samples.saturating_sub(1));
let p90 = my_round_trip_times[idx] as f64;
target.record_passive_rtt_sample(p90);
}
}
+6 -1
View File
@@ -336,7 +336,12 @@ fn main() -> Result<()> {
Err(e) => error!("Insight control channel failed to start: {:#}", e),
}
match lqos_stormguard::start_stormguard(bakery_sender_for_async).await {
match lqos_stormguard::start_stormguard(
bakery_sender_for_async,
shaped_devices_tracker::full_network_map_snapshot,
)
.await
{
Ok(_) => info!("StormGuard started successfully"),
Err(e) => error!("StormGuard failed to start: {:#}", e),
}
@@ -1,4 +1,18 @@
use crate::throughput_tracker::flow_data;
pub fn flow_map_data() -> Vec<(f64, f64, String, u64, f32)> {
flow_data::RECENT_FLOWS.lat_lon_endpoints()
}
pub fn endpoints_by_country_data() -> Vec<(
String,
lqos_utils::units::DownUpOrder<u64>,
[f32; 2],
String,
)> {
flow_data::RECENT_FLOWS.country_summary()
}
pub fn endpoint_latlon_data() -> Vec<(f64, f64, String, u64, f32)> {
flow_data::RECENT_FLOWS.lat_lon_endpoints()
}
+1 -1
View File
@@ -143,7 +143,7 @@ async fn one_second_cadence(
),
ticker_with_timeout(
"endpoint_latlon",
endpoint_latlon::endpoint_latlon(channels.clone(), bus_tx.clone())
endpoint_latlon::endpoint_latlon(channels.clone())
),
ticker_with_timeout(
"tree_summary",
@@ -1,15 +1,11 @@
use std::sync::Arc;
use crate::node_manager::local_api::flow_map;
use crate::node_manager::ws::messages::WsResponse;
use crate::node_manager::ws::publish_subscribe::PubSub;
use crate::node_manager::ws::published_channels::PublishedChannels;
use lqos_bus::{BusReply, BusRequest, BusResponse};
use tokio::sync::mpsc::Sender;
pub async fn endpoint_latlon(
channels: Arc<PubSub>,
bus_tx: Sender<(tokio::sync::oneshot::Sender<lqos_bus::BusReply>, BusRequest)>,
) {
pub async fn endpoint_latlon(channels: Arc<PubSub>) {
if !channels
.is_channel_alive(PublishedChannels::EndpointLatLon)
.await
@@ -17,29 +13,10 @@ pub async fn endpoint_latlon(
return;
}
let (tx, rx) = tokio::sync::oneshot::channel::<BusReply>();
let request = BusRequest::CurrentEndpointLatLon;
if let Err(e) = bus_tx.send((tx, request)).await {
tracing::warn!("EndpointLatLon: failed to send request to bus: {:?}", e);
return;
}
let replies = match rx.await {
Ok(r) => r,
Err(e) => {
tracing::warn!(
"EndpointLatLon: failed to receive response from bus: {:?}",
e
);
return;
}
let message = WsResponse::EndpointLatLon {
data: flow_map::endpoint_latlon_data(),
};
for reply in replies.responses.into_iter() {
if let BusResponse::CurrentLatLon(points) = reply {
let message = WsResponse::EndpointLatLon { data: points };
channels
.send(PublishedChannels::EndpointLatLon, message)
.await;
}
}
channels
.send(PublishedChannels::EndpointLatLon, message)
.await;
}
@@ -1,5 +1,6 @@
use std::sync::Arc;
use crate::node_manager::local_api::flow_map;
use crate::node_manager::ws::messages::{EtherProtocolsData, WsResponse};
use crate::node_manager::ws::publish_subscribe::PubSub;
use crate::node_manager::ws::published_channels::PublishedChannels;
@@ -8,7 +9,7 @@ use tokio::sync::mpsc::Sender;
pub async fn endpoints_by_country(
channels: Arc<PubSub>,
bus_tx: Sender<(tokio::sync::oneshot::Sender<lqos_bus::BusReply>, BusRequest)>,
_bus_tx: Sender<(tokio::sync::oneshot::Sender<lqos_bus::BusReply>, BusRequest)>,
) {
if !channels
.is_channel_alive(PublishedChannels::EndpointsByCountry)
@@ -17,30 +18,12 @@ pub async fn endpoints_by_country(
return;
}
let (tx, rx) = tokio::sync::oneshot::channel::<BusReply>();
let request = BusRequest::CurrentEndpointsByCountry;
if let Err(e) = bus_tx.send((tx, request)).await {
tracing::warn!("EndpointsByCountry: failed to send request to bus: {:?}", e);
return;
}
let replies = match rx.await {
Ok(r) => r,
Err(e) => {
tracing::warn!(
"EndpointsByCountry: failed to receive throughput from bus: {:?}",
e
);
return;
}
let message = WsResponse::EndpointsByCountry {
data: flow_map::endpoints_by_country_data(),
};
for reply in replies.responses.into_iter() {
if let BusResponse::CurrentEndpointsByCountry(countries) = reply {
let message = WsResponse::EndpointsByCountry { data: countries };
channels
.send(PublishedChannels::EndpointsByCountry, message)
.await;
}
}
channels
.send(PublishedChannels::EndpointsByCountry, message)
.await;
}
pub async fn ether_protocols(
@@ -150,17 +150,17 @@ pub fn get_one_network_map_layer(parent_idx: usize) -> BusResponse {
}
}
pub fn get_full_network_map() -> BusResponse {
pub fn full_network_map_snapshot() -> Vec<(usize, NetworkJsonTransport)> {
let nj = NETWORK_JSON.read();
let data = {
nj.get_nodes_when_ready()
.iter()
.enumerate()
.map(|(i, n)| (i, n.clone_to_transit()))
.collect::<Vec<(usize, NetworkJsonTransport)>>()
};
nj.get_nodes_when_ready()
.iter()
.enumerate()
.map(|(i, n)| (i, n.clone_to_transit()))
.collect()
}
BusResponse::NetworkMap(data)
pub fn get_full_network_map() -> BusResponse {
BusResponse::NetworkMap(full_network_map_snapshot())
}
pub fn get_top_n_root_queues(n_queues: usize) -> BusResponse {
@@ -1212,7 +1212,6 @@ pub fn current_endpoints_by_country() -> BusResponse {
/// Current endpoint lat/lon
pub fn current_lat_lon() -> BusResponse {
let summary = flow_data::RECENT_FLOWS.lat_lon_endpoints();
debug!("CurrentLatLon payload: {:?}", summary);
BusResponse::CurrentLatLon(summary)
}
+43 -11
View File
@@ -23,12 +23,15 @@ use lqos_overrides::{NetworkAdjustment, OverrideFile, OverrideLayer, OverrideSto
use lqos_utils::hash_to_i64;
use lqos_utils::units::DownUpOrder;
use lqos_utils::unix_time::{time_since_boot, unix_now};
use parking_lot::RwLock;
use std::collections::VecDeque;
use std::sync::OnceLock;
use std::time::{Duration, Instant};
use tracing::{debug, warn};
static TREEGUARD_SENDER: OnceLock<Sender<TreeguardCommand>> = OnceLock::new();
static TREEGUARD_STATUS_CACHE: OnceLock<RwLock<TreeguardStatusData>> = OnceLock::new();
static TREEGUARD_ACTIVITY_CACHE: OnceLock<RwLock<Vec<TreeguardActivityEntry>>> = OnceLock::new();
const ACTIVITY_RING_CAPACITY: usize = 200;
const UTIL_EWMA_ALPHA: f64 = 0.1;
@@ -60,6 +63,9 @@ pub(crate) fn start_treeguard_actor(
return Ok(());
}
let _ = TREEGUARD_STATUS_CACHE.set(RwLock::new(empty_status_snapshot()));
let _ = TREEGUARD_ACTIVITY_CACHE.set(RwLock::new(Vec::new()));
let (tx, rx) = crossbeam_channel::bounded::<TreeguardCommand>(64);
let _ = TREEGUARD_SENDER.set(tx);
@@ -70,6 +76,14 @@ pub(crate) fn start_treeguard_actor(
Ok(())
}
pub(crate) fn cached_status_snapshot() -> Option<TreeguardStatusData> {
Some(TREEGUARD_STATUS_CACHE.get()?.read().clone())
}
pub(crate) fn cached_activity_snapshot() -> Option<Vec<TreeguardActivityEntry>> {
Some(TREEGUARD_ACTIVITY_CACHE.get()?.read().clone())
}
/// Requests a status snapshot from the TreeGuard actor.
///
/// This function is not pure: it sends a message to the TreeGuard actor thread.
@@ -110,18 +124,9 @@ fn treeguard_actor_loop(
) {
debug!("TreeGuard actor started");
let mut status = TreeguardStatusData {
enabled: false,
dry_run: true,
cpu_max_pct: None,
managed_nodes: 0,
managed_circuits: 0,
virtualized_nodes: 0,
fq_codel_circuits: 0,
last_action_summary: None,
warnings: Vec::new(),
};
let mut status = empty_status_snapshot();
let mut activity: VecDeque<TreeguardActivityEntry> = VecDeque::new();
update_cached_snapshots(&status, &activity);
let mut runtime_state = TreeguardRuntimeState::default();
@@ -143,6 +148,7 @@ fn treeguard_actor_loop(
&mut tick_seconds,
&mut runtime_state,
);
update_cached_snapshots(&status, &activity);
}
Err(crossbeam_channel::RecvTimeoutError::Disconnected) => {
warn!("TreeGuard actor command channel disconnected; exiting actor");
@@ -171,6 +177,32 @@ fn handle_command(
}
}
fn empty_status_snapshot() -> TreeguardStatusData {
TreeguardStatusData {
enabled: false,
dry_run: true,
cpu_max_pct: None,
managed_nodes: 0,
managed_circuits: 0,
virtualized_nodes: 0,
fq_codel_circuits: 0,
last_action_summary: None,
warnings: Vec::new(),
}
}
fn update_cached_snapshots(
status: &TreeguardStatusData,
activity: &VecDeque<TreeguardActivityEntry>,
) {
if let Some(cache) = TREEGUARD_STATUS_CACHE.get() {
*cache.write() = status.clone();
}
if let Some(cache) = TREEGUARD_ACTIVITY_CACHE.get() {
*cache.write() = activity.iter().cloned().rev().collect();
}
}
#[derive(Default)]
struct TreeguardRuntimeState {
link_states: FxHashMap<String, LinkState>,
+8
View File
@@ -9,6 +9,10 @@ use lqos_config::load_config;
/// This function is not pure: it sends a request to the TreeGuard actor, and may fall back to
/// reading the current configuration from disk (via the config cache).
pub async fn treeguard_status_snapshot() -> TreeguardStatusData {
if let Some(snapshot) = actor::cached_status_snapshot() {
return snapshot;
}
if let Some(snapshot) = actor::request_status_snapshot().await {
return snapshot;
}
@@ -71,5 +75,9 @@ pub async fn treeguard_status_snapshot() -> TreeguardStatusData {
/// This function is not pure: it sends a request to the TreeGuard actor.
/// If the actor isn't available, it returns an empty list.
pub async fn treeguard_activity_snapshot() -> Vec<TreeguardActivityEntry> {
if let Some(snapshot) = actor::cached_activity_snapshot() {
return snapshot;
}
actor::request_activity_snapshot().await.unwrap_or_default()
}