fix(shaper): restore base rates and recover recreated CAKE
This commit is contained in:
4 files changed
+240
-24
No files matched your search
+9
-2
@@ -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
@@ -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)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(¤t, 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+60
-5
@@ -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) =
|
||||||
&mut self.stats_provider,
|
match get_interface_stats(
|
||||||
&self.settings.network.upload_interface,
|
&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()?;
|
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() {
|
||||||
|
|||||||
Reference in new issue
Block a user