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::metrics::{Metric, MetricsSender};
use crate::pinger::PingReply; use crate::pinger::PingReply;
use crate::settings::Settings; use crate::settings::Settings;
use flume::{Receiver, Sender}; use flume::{Receiver, RecvTimeoutError, Sender};
use log::{debug, info}; use log::{debug, info};
use std::collections::HashMap; use std::collections::HashMap;
use std::net::IpAddr; use std::net::IpAddr;
@@ -165,7 +165,14 @@ impl Baseliner {
return Ok(()); 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 reflector = reply.reflector;
let (state, anomaly) = process_reply( let (state, anomaly) = process_reply(
&mut reflectors, &mut reflectors,
+125 -13
View File
@@ -121,16 +121,24 @@ fn initialize_shaper_with_settle_time<T: TrafficControlBackend>(
settings.network.upload_min_kbits(), settings.network.upload_min_kbits(),
); );
traffic_control.set_rate( if let Err(error) = traffic_control.set_rate(
&down, &down,
settings.network.download_min_kbits() as u64, settings.network.download_min_kbits() as u64,
settings.advanced_settings.dry_run, 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, &up,
settings.network.upload_min_kbits() as u64, settings.network.upload_min_kbits() as u64,
settings.advanced_settings.dry_run, settings.advanced_settings.dry_run,
)?; ) {
SHUTDOWN.store(true, Ordering::Relaxed);
restore_shaper(settings, traffic_control, &down, &up);
return Err(error.into());
}
info!( info!(
"Sleeping for {} seconds to give the shaper a chance to control existing bloat", "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> { fn setup_reflectors(settings: &Settings) -> anyhow::Result<ReflectorSetup> {
let configured = settings.load_reflectors()?; let configured = settings.load_reflectors()?;
let configured_count = configured.len(); let configured_count = configured.len();
@@ -338,6 +360,12 @@ fn run(settings: &Settings) -> anyhow::Result<()> {
let mut main_traffic_control = traffic_control_backend(); let mut main_traffic_control = traffic_control_backend();
let (down_shaper, up_shaper) = initialize_shaper(settings, &mut main_traffic_control)?; 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 err_tx = error_tx.clone();
let listener_peers = Arc::clone(&reflector_peers); 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. // 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( let mut ratecontroller = Ratecontroller::new(
settings.clone(), settings.clone(),
@@ -420,7 +454,7 @@ fn run(settings: &Settings) -> anyhow::Result<()> {
)?; )?;
let err_tx = error_tx.clone(); let err_tx = error_tx.clone();
thread::Builder::new() let ratecontroller_thread_handle = thread::Builder::new()
.name("ratecontroller".to_string()) .name("ratecontroller".to_string())
.spawn(move || { .spawn(move || {
if let Err(error) = ratecontroller.run() { if let Err(error) = ratecontroller.run() {
@@ -449,17 +483,13 @@ fn run(settings: &Settings) -> anyhow::Result<()> {
drop(main_event_metrics); drop(main_event_metrics);
drop(metrics_tx); drop(metrics_tx);
let _ = ratecontroller_thread_handle.join();
drop(shaper_guard);
if let Some(handle) = metrics_thread_handle { if let Some(handle) = metrics_thread_handle {
let _ = handle.join(); let _ = handle.join();
} }
restore_shaper(
settings,
&mut main_traffic_control,
&down_shaper,
&up_shaper,
);
result result
} }
@@ -476,3 +506,85 @@ fn main() -> anyhow::Result<()> {
run(&settings) 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::io;
use std::str::Utf8Error; use std::str::Utf8Error;
use log::info; use log::{info, warn};
use netlink_bindings::{rt_link, tc}; use netlink_bindings::{rt_link, tc};
use netlink_socket2::NetlinkSocket; use netlink_socket2::NetlinkSocket;
use std::thread::sleep;
use std::time::{Duration, Instant};
use thiserror::Error; use thiserror::Error;
use crate::SHUTDOWN;
use crate::platform::{InterfaceStats, InterfaceStatsProvider, TrafficControlBackend}; use crate::platform::{InterfaceStats, InterfaceStatsProvider, TrafficControlBackend};
use std::sync::atomic::Ordering;
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub(crate) enum NetlinkError { pub(crate) enum NetlinkError {
@@ -36,12 +40,29 @@ pub(crate) enum NetlinkError {
Utf8Error(#[from] Utf8Error), Utf8Error(#[from] Utf8Error),
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Qdisc { pub(crate) struct Qdisc {
ifname: String,
ifindex: i32, ifindex: i32,
parent: u32, 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)] #[derive(Debug, Default)]
pub(crate) struct Netlink {} pub(crate) struct Netlink {}
@@ -96,6 +117,7 @@ impl Netlink {
== "cake" == "cake"
{ {
return Ok(Qdisc { return Ok(Qdisc {
ifname: ifname.to_owned(),
ifindex, ifindex,
parent: header.parent, parent: header.parent,
}); });
@@ -111,7 +133,7 @@ impl Netlink {
} }
fn set_qdisc_rate( fn set_qdisc_rate(
qdisc: Qdisc, qdisc: &Qdisc,
bandwidth_kbit: u64, bandwidth_kbit: u64,
dry_run: bool, dry_run: bool,
) -> Result<(), NetlinkError> { ) -> Result<(), NetlinkError> {
@@ -167,6 +189,26 @@ impl TrafficControlBackend for Netlink {
bandwidth_kbit: u64, bandwidth_kbit: u64,
dry_run: bool, dry_run: bool,
) -> Result<(), Self::Error> { ) -> 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 lastchg_t = Instant::now();
let mut lastdump_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()?; self.request_initial_rates()?;
@@ -336,7 +338,22 @@ impl<S: InterfaceStatsProvider, T: TrafficControlBackend> Ratecontroller<S, T> {
info!("Rate controller shutting down"); info!("Rate controller shutting down");
return Ok(()); 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(); let now_t = Instant::now();
if now_t.duration_since(lastchg_t).as_secs_f64() 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 // if it's been long enough, and the stats indicate needing to change speeds
// change speeds here // 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, &mut self.stats_provider,
&self.settings.network.upload_interface, &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()?; self.update_deltas()?;
if self.state_dl.deltas.is_empty() || self.state_ul.deltas.is_empty() { if self.state_dl.deltas.is_empty() || self.state_ul.deltas.is_empty() {