From ddf78a0f16420c02735faed6a00ef0da8b1c7794 Mon Sep 17 00:00:00 2001 From: Herbert Wolverson Date: Thu, 30 Jul 2026 09:59:57 -0500 Subject: [PATCH] Fix lazy queue memory preflight warnings --- .../v2.0/libreqos-backend-architecture-es.md | 2 +- docs/v2.0/libreqos-backend-architecture.md | 2 +- src/LibreQoS.py | 4 ++ src/rust/lqos_bakery/src/lib.rs | 62 +++++++++++++++++-- src/rust/lqos_python/src/lib.rs | 15 +++-- src/rust/lqosd/src/main.rs | 1 + .../js_build/src/dashlets/bakery_activity.js | 3 + .../js_build/src/dashlets/bakery_capacity.js | 3 + 8 files changed, 81 insertions(+), 11 deletions(-) diff --git a/docs-es/v2.0/libreqos-backend-architecture-es.md b/docs-es/v2.0/libreqos-backend-architecture-es.md index 1ca79e39..b804136c 100644 --- a/docs-es/v2.0/libreqos-backend-architecture-es.md +++ b/docs-es/v2.0/libreqos-backend-architecture-es.md @@ -228,7 +228,7 @@ Flujo general: Controles clave: -1. `lazy_queues`: diferir creación de partes de la jerarquía hasta uso real. +1. `lazy_queues`: diferir la creación de partes de la jerarquía hasta el uso real. Cuando está activado, una comprobación previa que solo indica falta de memoria se informa como advertencia para que la actualización pueda continuar; asegúrese de que el host tenga RAM suficiente. 2. `lazy_expire_seconds`: remover estado de cola inactivo tras timeout. Efecto práctico: diff --git a/docs/v2.0/libreqos-backend-architecture.md b/docs/v2.0/libreqos-backend-architecture.md index 195317b7..29f9bc6d 100644 --- a/docs/v2.0/libreqos-backend-architecture.md +++ b/docs/v2.0/libreqos-backend-architecture.md @@ -261,7 +261,7 @@ High-level flow: Key controls: -1. `lazy_queues`: defer creating parts of the hierarchy until active use. +1. `lazy_queues`: defer creating parts of the hierarchy until active use. When enabled, a memory-only queue preflight result is reported as a warning so the refresh can continue; ensure the host still has adequate RAM. 2. `lazy_expire_seconds`: remove inactive queue state after timeout. Practical effect: diff --git a/src/LibreQoS.py b/src/LibreQoS.py index 885e7bbc..f5244a6a 100755 --- a/src/LibreQoS.py +++ b/src/LibreQoS.py @@ -2860,6 +2860,10 @@ def refreshShapers(): }, "TC_QDISC_CAPACITY", ) + elif qdiscBudgetEstimate.get("memory_warning_only"): + preflightSummary = qdiscBudgetEstimate.get("summary") or "" + logging.warning(preflightSummary) + print("WARNING: " + preflightSummary) # Save queuingStructure queuingStructure = {} diff --git a/src/rust/lqos_bakery/src/lib.rs b/src/rust/lqos_bakery/src/lib.rs index c2861bdd..d55a3b21 100644 --- a/src/rust/lqos_bakery/src/lib.rs +++ b/src/rust/lqos_bakery/src/lib.rs @@ -1693,6 +1693,8 @@ pub struct BakeryPreflightSnapshot { pub memory_guard_min_available_bytes: u64, /// Whether the memory preflight passed. pub memory_ok: bool, + /// Whether the memory-only failure is an allowed lazy-queue warning. + pub memory_warning_only: bool, /// Per-interface planned qdisc counts. pub interfaces: Vec, } @@ -2459,18 +2461,27 @@ fn refresh_live_capacity_snapshot(config: &Config, force: bool) { /// This function is not pure: it updates retained in-memory Bakery telemetry state. pub fn record_qdisc_preflight_snapshot(snapshot: BakeryPreflightSnapshot) { let ok = snapshot.ok; + let memory_warning_only = snapshot.memory_warning_only; let summary = snapshot.message.clone(); { let mut state = telemetry_state().write(); state.preflight = Some(snapshot); } push_bakery_event( - if ok { + if memory_warning_only { + "preflight_warning" + } else if ok { "preflight_ok" } else { "preflight_blocked" }, - if ok { "info" } else { "warning" }, + if memory_warning_only { + "warning" + } else if ok { + "info" + } else { + "warning" + }, summary, ); } @@ -2571,15 +2582,17 @@ pub struct QdiscBudgetEstimate { pub memory_guard_min_available_bytes: u64, /// Whether the memory preflight passed. pub memory_ok: bool, + /// Whether lazy queue mode makes a memory-only preflight failure non-blocking. + pub memory_warning_only: bool, } impl QdiscBudgetEstimate { - /// Returns `true` when all planned per-interface counts fit within the safe budget. + /// Returns `true` when the qdisc count fits and memory preflight passes or is a lazy-queue warning. pub fn ok(&self) -> bool { self.interfaces .values() .all(|count| *count <= self.safe_budget) - && self.memory_ok + && (self.memory_ok || self.memory_warning_only) } } @@ -2723,6 +2736,14 @@ pub fn estimate_full_reload_auto_qdisc_budget( .saturating_sub(memory_guard_min_available_bytes) >= estimated_total_memory_bytes }); + let qdisc_counts_fit = interfaces + .values() + .all(|count| *count <= SAFE_QDISC_BUDGET_PER_INTERFACE); + let memory_warning_only = lazy_queue_memory_preflight_is_warning( + config.queues.lazy_queues.as_ref(), + qdisc_counts_fit, + memory_ok, + ); QdiscBudgetEstimate { interfaces, @@ -2733,9 +2754,23 @@ pub fn estimate_full_reload_auto_qdisc_budget( memory_snapshot, memory_guard_min_available_bytes, memory_ok, + memory_warning_only, } } +fn lazy_queue_memory_preflight_is_warning( + lazy_queue_mode: Option<&LazyQueueMode>, + qdisc_counts_fit: bool, + memory_ok: bool, +) -> bool { + qdisc_counts_fit + && !memory_ok + && matches!( + lazy_queue_mode, + Some(LazyQueueMode::Htb | LazyQueueMode::Full) + ) +} + fn desired_shaping_tree_active(config: &Arc) -> bool { !config.queues.queue_mode.is_observe() } @@ -14356,6 +14391,25 @@ mod tests { ); } + #[test] + fn memory_warning_only_allows_preflight_to_continue() { + let mut estimate = QdiscBudgetEstimate { + interfaces: BTreeMap::from([("eth0".to_string(), 1)]), + interface_details: BTreeMap::new(), + safe_budget: SAFE_QDISC_BUDGET_PER_INTERFACE, + hard_limit: HARD_QDISC_HANDLE_LIMIT_PER_INTERFACE, + estimated_total_memory_bytes: 1, + memory_snapshot: None, + memory_guard_min_available_bytes: BAKERY_MEMORY_GUARD_MIN_AVAILABLE_BYTES, + memory_ok: false, + memory_warning_only: true, + }; + + assert!(estimate.ok()); + estimate.memory_warning_only = false; + assert!(!estimate.ok()); + } + #[test] fn command_qdisc_memory_estimate_deduplicates_qdisc_identity() { let commands = vec![ diff --git a/src/rust/lqos_python/src/lib.rs b/src/rust/lqos_python/src/lib.rs index 5daa7c55..2f82cb4e 100644 --- a/src/rust/lqos_python/src/lib.rs +++ b/src/rust/lqos_python/src/lib.rs @@ -3646,11 +3646,15 @@ impl Bakery { .interfaces .values() .all(|count| *count <= estimate.safe_budget); - let preflight_status = match (qdisc_counts_fit, estimate.memory_ok) { - (true, true) => "fits preflight", - (false, true) => "exceeds qdisc-count preflight", - (true, false) => "passed qdisc-count preflight but failed memory preflight", - (false, false) => "exceeds qdisc-count preflight and failed memory preflight", + let preflight_status = if estimate.memory_warning_only { + "passed qdisc-count preflight; memory preflight is a non-blocking lazy-queue warning" + } else { + match (qdisc_counts_fit, estimate.memory_ok) { + (true, true) => "fits preflight", + (false, true) => "exceeds qdisc-count preflight", + (true, false) => "passed qdisc-count preflight but failed memory preflight", + (false, false) => "exceeds qdisc-count preflight and failed memory preflight", + } }; let memory_summary = if let Some(snapshot) = estimate.memory_snapshot.as_ref() { let required_available_bytes = estimate @@ -3738,6 +3742,7 @@ impl Bakery { estimate.estimated_total_memory_bytes, )?; result.set_item("memory_ok", estimate.memory_ok)?; + result.set_item("memory_warning_only", estimate.memory_warning_only)?; result.set_item( "memory_guard_min_available_bytes", estimate.memory_guard_min_available_bytes, diff --git a/src/rust/lqosd/src/main.rs b/src/rust/lqosd/src/main.rs index 180f1de9..47a41bbb 100644 --- a/src/rust/lqosd/src/main.rs +++ b/src/rust/lqosd/src/main.rs @@ -907,6 +907,7 @@ fn handle_bus_requests(requests: &[BusRequest], responses: &mut Vec memory_available_bytes: *memory_available_bytes, memory_guard_min_available_bytes: *memory_guard_min_available_bytes, memory_ok: *memory_ok, + memory_warning_only: *ok && !*memory_ok, interfaces: interfaces .iter() .map(|entry| lqos_bakery::BakeryCapacityInterfaceSnapshot { diff --git a/src/rust/lqosd/src/node_manager/js_build/src/dashlets/bakery_activity.js b/src/rust/lqosd/src/node_manager/js_build/src/dashlets/bakery_activity.js index 4273f3b4..c1bd925b 100644 --- a/src/rust/lqosd/src/node_manager/js_build/src/dashlets/bakery_activity.js +++ b/src/rust/lqosd/src/node_manager/js_build/src/dashlets/bakery_activity.js @@ -99,6 +99,9 @@ function classifyEvent(entry) { } else if (event === "preflight_ok") { outcome = "Passed"; outcomeClass = "bg-success-subtle text-success border border-success-subtle"; + } else if (event === "preflight_warning") { + outcome = "Memory Warning"; + outcomeClass = "bg-warning-subtle text-warning border border-warning-subtle"; } else if (event === "preflight_blocked") { outcome = "Blocked"; outcomeClass = "bg-warning-subtle text-warning border border-warning-subtle"; diff --git a/src/rust/lqosd/src/node_manager/js_build/src/dashlets/bakery_capacity.js b/src/rust/lqosd/src/node_manager/js_build/src/dashlets/bakery_capacity.js index 1e342d0a..e1e3125d 100644 --- a/src/rust/lqosd/src/node_manager/js_build/src/dashlets/bakery_capacity.js +++ b/src/rust/lqosd/src/node_manager/js_build/src/dashlets/bakery_capacity.js @@ -37,6 +37,9 @@ function bakeryMemoryBadge(preflight) { if (preflight.memoryOk) { return mkBadge("Memory OK", "bg-success-subtle text-success border border-success-subtle", preflight.message || ""); } + if (preflight.ok) { + return mkBadge("Memory Warning", "bg-warning-subtle text-warning border border-warning-subtle", preflight.message || ""); + } return mkBadge("Memory Guard", "bg-danger-subtle text-danger border border-danger-subtle", preflight.message || ""); }