fix(shaper): restore base rates and recover recreated CAKE
Build / build (pull_request) Canceled after 0s
DevSkim / DevSkim (pull_request) Canceled after 0s
rust-clippy analyze / Run rust-clippy analyzing (pull_request) Canceled after 0s

This commit is contained in:
Nils Andreas Svee committed 2026-09-24 17:21:48 +07:00
1 parent 8fb051d48f
commit 708d88fc57
4 files changed
+237 -21

No files matched your search

+9 -2
View File
@@ -9,7 +9,7 @@ use crate::SHUTDOWN;
use crate::metrics::{Metric, MetricsSender};
use crate::pinger::PingReply;
use crate::settings::Settings;
use flume::{Receiver, Sender};
use flume::{Receiver, RecvTimeoutError, Sender};
use log::{debug, info};
use std::collections::HashMap;
use std::net::IpAddr;
@@ -165,7 +165,14 @@ impl Baseliner {
return Ok(());
}
let reply = self.stats_rx.recv()?;
let reply = match self.stats_rx.recv_timeout(Duration::from_millis(500)) {
Ok(reply) => reply,
Err(RecvTimeoutError::Timeout) => continue,
Err(RecvTimeoutError::Disconnected) if SHUTDOWN.load(Ordering::Relaxed) => {
return Ok(());
}
Err(error) => return Err(error.into()),
};
let reflector = reply.reflector;
let (state, anomaly) = process_reply(
&mut reflectors,
+125 -13
View File
@@ -121,16 +121,24 @@ fn initialize_shaper_with_settle_time<T: TrafficControlBackend>(
settings.network.upload_min_kbits(),
);
traffic_control.set_rate(
if let Err(error) = traffic_control.set_rate(
&down,
settings.network.download_min_kbits() as u64,
settings.advanced_settings.dry_run,
)?;
traffic_control.set_rate(
) {
SHUTDOWN.store(true, Ordering::Relaxed);
restore_shaper(settings, traffic_control, &down, &up);
return Err(error.into());
}
if let Err(error) = traffic_control.set_rate(
&up,
settings.network.upload_min_kbits() as u64,
settings.advanced_settings.dry_run,
)?;
) {
SHUTDOWN.store(true, Ordering::Relaxed);
restore_shaper(settings, traffic_control, &down, &up);
return Err(error.into());
}
info!(
"Sleeping for {} seconds to give the shaper a chance to control existing bloat",
@@ -188,6 +196,20 @@ fn restore_shaper<T: TrafficControlBackend>(
}
}
struct ShaperGuard<'a, T: TrafficControlBackend> {
settings: &'a Settings,
traffic_control: &'a mut T,
down: T::Handle,
up: T::Handle,
}
impl<T: TrafficControlBackend> Drop for ShaperGuard<'_, T> {
fn drop(&mut self) {
SHUTDOWN.store(true, Ordering::Relaxed);
restore_shaper(self.settings, self.traffic_control, &self.down, &self.up);
}
}
fn setup_reflectors(settings: &Settings) -> anyhow::Result<ReflectorSetup> {
let configured = settings.load_reflectors()?;
let configured_count = configured.len();
@@ -338,6 +360,12 @@ fn run(settings: &Settings) -> anyhow::Result<()> {
let mut main_traffic_control = traffic_control_backend();
let (down_shaper, up_shaper) = initialize_shaper(settings, &mut main_traffic_control)?;
let shaper_guard = ShaperGuard {
settings,
traffic_control: &mut main_traffic_control,
down: down_shaper,
up: up_shaper,
};
let err_tx = error_tx.clone();
let listener_peers = Arc::clone(&reflector_peers);
@@ -409,7 +437,13 @@ fn run(settings: &Settings) -> anyhow::Result<()> {
}
// Give the baseliner time to collect initial samples before adjusting rates.
sleep(Duration::from_secs(10));
let baseline_start = Instant::now();
while baseline_start.elapsed() < Duration::from_secs(10) {
if SHUTDOWN.load(Ordering::Relaxed) {
return Ok(());
}
sleep(Duration::from_millis(100));
}
let mut ratecontroller = Ratecontroller::new(
settings.clone(),
@@ -420,7 +454,7 @@ fn run(settings: &Settings) -> anyhow::Result<()> {
)?;
let err_tx = error_tx.clone();
thread::Builder::new()
let ratecontroller_thread_handle = thread::Builder::new()
.name("ratecontroller".to_string())
.spawn(move || {
if let Err(error) = ratecontroller.run() {
@@ -449,17 +483,13 @@ fn run(settings: &Settings) -> anyhow::Result<()> {
drop(main_event_metrics);
drop(metrics_tx);
let _ = ratecontroller_thread_handle.join();
drop(shaper_guard);
if let Some(handle) = metrics_thread_handle {
let _ = handle.join();
}
restore_shaper(
settings,
&mut main_traffic_control,
&down_shaper,
&up_shaper,
);
result
}
@@ -476,3 +506,85 @@ fn main() -> anyhow::Result<()> {
run(&settings)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::settings::{
AdvancedSettings, NetworkSettings, ObservabilitySettings, OutputSettings,
};
#[derive(Default)]
struct FakeControl {
calls: Vec<(String, u64)>,
fail_upload_min: bool,
}
impl TrafficControlBackend for FakeControl {
type Error = std::io::Error;
type Handle = String;
fn find_shaper(&mut self, interface: &str) -> Result<Self::Handle, Self::Error> {
Ok(interface.into())
}
fn set_rate(
&mut self,
shaper: &Self::Handle,
rate: u64,
_: bool,
) -> Result<(), Self::Error> {
self.calls.push((shaper.clone(), rate));
if self.fail_upload_min && shaper == "eth0" && rate == 1_000 {
return Err(std::io::Error::other("simulated startup failure"));
}
Ok(())
}
}
#[test]
fn startup_failures_restore_both_base_rates() {
let settings = Settings {
network: NetworkSettings {
download_interface: "ifb0".into(),
upload_interface: "eth0".into(),
download_base_kbits: 10_000.0,
download_min_percent: 20.0,
upload_base_kbits: 5_000.0,
upload_min_percent: 20.0,
},
output: OutputSettings::default(),
observability: ObservabilitySettings::default(),
advanced_settings: AdvancedSettings::default(),
};
let mut control = FakeControl {
fail_upload_min: true,
..FakeControl::default()
};
assert!(
initialize_shaper_with_settle_time(&settings, &mut control, Duration::ZERO).is_err()
);
SHUTDOWN.store(false, Ordering::Relaxed);
assert_eq!(
control.calls[2..],
[("ifb0".into(), 10_000), ("eth0".into(), 5_000)]
);
control.calls.clear();
control.fail_upload_min = false;
let (down, up) =
initialize_shaper_with_settle_time(&settings, &mut control, Duration::ZERO).unwrap();
let guard = ShaperGuard {
settings: &settings,
traffic_control: &mut control,
down,
up,
};
drop(guard);
SHUTDOWN.store(false, Ordering::Relaxed);
assert_eq!(
control.calls[2..],
[("ifb0".into(), 10_000), ("eth0".into(), 5_000)]
);
}
}
+46 -4
View File
@@ -5,12 +5,16 @@
use std::io;
use std::str::Utf8Error;
use log::info;
use log::{info, warn};
use netlink_bindings::{rt_link, tc};
use netlink_socket2::NetlinkSocket;
use std::thread::sleep;
use std::time::{Duration, Instant};
use thiserror::Error;
use crate::SHUTDOWN;
use crate::platform::{InterfaceStats, InterfaceStatsProvider, TrafficControlBackend};
use std::sync::atomic::Ordering;
#[derive(Debug, Error)]
pub(crate) enum NetlinkError {
@@ -36,12 +40,29 @@ pub(crate) enum NetlinkError {
Utf8Error(#[from] Utf8Error),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Qdisc {
ifname: String,
ifindex: i32,
parent: u32,
}
impl NetlinkError {
fn is_qdisc_loss(&self) -> bool {
match self {
Self::InterfaceNotFound(_) | Self::NoQdiscFound(_) => true,
Self::Netlink(error) => {
matches!(error.raw_os_error(), Some(libc::ENODEV | libc::ENOENT))
}
Self::Reply(error) => matches!(
error.as_io_error().raw_os_error(),
Some(libc::ENODEV | libc::ENOENT)
),
_ => false,
}
}
}
#[derive(Debug, Default)]
pub(crate) struct Netlink {}
@@ -96,6 +117,7 @@ impl Netlink {
== "cake"
{
return Ok(Qdisc {
ifname: ifname.to_owned(),
ifindex,
parent: header.parent,
});
@@ -111,7 +133,7 @@ impl Netlink {
}
fn set_qdisc_rate(
qdisc: Qdisc,
qdisc: &Qdisc,
bandwidth_kbit: u64,
dry_run: bool,
) -> Result<(), NetlinkError> {
@@ -167,6 +189,26 @@ impl TrafficControlBackend for Netlink {
bandwidth_kbit: u64,
dry_run: bool,
) -> Result<(), Self::Error> {
Self::set_qdisc_rate(*shaper, bandwidth_kbit, dry_run)
let mut last_warning = None;
loop {
let result = Self::qdisc_from_ifname(&shaper.ifname)
.and_then(|current| Self::set_qdisc_rate(&current, bandwidth_kbit, dry_run));
match result {
Ok(()) => return Ok(()),
Err(error) if error.is_qdisc_loss() && !SHUTDOWN.load(Ordering::Relaxed) => {
if last_warning
.is_none_or(|at: Instant| at.elapsed() >= Duration::from_secs(30))
{
warn!(
"CAKE on {} is unavailable: {error}; waiting for SQM",
shaper.ifname
);
last_warning = Some(Instant::now());
}
sleep(Duration::from_millis(500));
}
Err(error) => return Err(error),
}
}
}
}
+57 -2
View File
@@ -297,6 +297,8 @@ impl<S: InterfaceStatsProvider, T: TrafficControlBackend> Ratecontroller<S, T> {
let mut lastchg_t = Instant::now();
let mut lastdump_t = Instant::now();
let mut last_reassert_t = Instant::now();
let mut stats_missing = false;
self.request_initial_rates()?;
@@ -336,7 +338,22 @@ impl<S: InterfaceStatsProvider, T: TrafficControlBackend> Ratecontroller<S, T> {
info!("Rate controller shutting down");
return Ok(());
}
sleep(sleep_time);
let wake_at = Instant::now() + sleep_time;
while Instant::now() < wake_at {
if SHUTDOWN.load(Ordering::Relaxed) {
info!("Rate controller shutting down");
return Ok(());
}
sleep(
wake_at
.saturating_duration_since(Instant::now())
.min(Duration::from_millis(100)),
);
}
if SHUTDOWN.load(Ordering::Relaxed) {
info!("Rate controller shutting down");
return Ok(());
}
let now_t = Instant::now();
if now_t.duration_since(lastchg_t).as_secs_f64()
@@ -345,10 +362,48 @@ impl<S: InterfaceStatsProvider, T: TrafficControlBackend> Ratecontroller<S, T> {
// if it's been long enough, and the stats indicate needing to change speeds
// change speeds here
(self.state_dl.current_bytes, self.state_ul.current_bytes) = get_interface_stats(
(self.state_dl.current_bytes, self.state_ul.current_bytes) =
match get_interface_stats(
&mut self.stats_provider,
&self.settings.network.upload_interface,
) {
Ok(stats) => {
if stats_missing {
info!("Interface statistics available again");
stats_missing = false;
}
stats
}
Err(error) => {
if !stats_missing {
warn!(
"Interface statistics unavailable: {error}; waiting for interface recovery"
);
stats_missing = true;
}
continue;
}
};
if SHUTDOWN.load(Ordering::Relaxed) {
return Ok(());
}
// SQM may replace CAKE while the desired rate remains unchanged.
if !self.settings.advanced_settings.dry_run
&& now_t.duration_since(last_reassert_t) >= Duration::from_secs(5)
{
self.traffic_control.set_rate(
&self.state_dl.shaper,
self.state_dl.current_rate.round() as u64,
false,
)?;
self.traffic_control.set_rate(
&self.state_ul.shaper,
self.state_ul.current_rate.round() as u64,
false,
)?;
last_reassert_t = now_t;
}
self.update_deltas()?;
if self.state_dl.deltas.is_empty() || self.state_ul.deltas.is_empty() {