This commit is contained in:
rchac committed 2026-04-19 20:28:56 -06:00
1 parent ab7c9531ae
commit 392f122cd3
2 files changed
+191 -2

No files matched your search

+38 -2
View File
@@ -4,7 +4,7 @@ import csv
import io
import json
import chardet
from LibreQoS import ValidationFailure, refreshShapers, refreshShapersUpdateOnly
from LibreQoS import RefreshFailure, ValidationFailure, refreshShapers, refreshShapersUpdateOnly
import subprocess
import sys
import tempfile
@@ -1026,7 +1026,12 @@ def importAndShapeFullReload():
return False
publish_scheduler_progress(True, "initial_shaping_reload", "Refreshing shaper state", 5, SCHEDULER_STARTUP_STEP_COUNT)
if not enable_insight_topology():
refreshShapers()
try:
refreshShapers()
except Exception as e:
if _defer_stale_shaping_inputs_failure(e, startup=True):
return False
raise
shaping_runtime_hash = calculate_shaping_runtime_hash()
else:
shaping_runtime_hash = calculate_shaping_runtime_hash()
@@ -1072,6 +1077,8 @@ def importAndShapePartialReload():
report_scheduler_validation_failure("Scheduled shaping refresh blocked by validation", e)
return
except Exception as e:
if _defer_stale_shaping_inputs_failure(e, startup=False):
return
report_scheduler_runtime_failure("Scheduled shaping refresh failed", e)
return
shaping_runtime_hash = calculate_shaping_runtime_hash()
@@ -1223,6 +1230,29 @@ def _topology_runtime_not_ready_is_transient(detail: str) -> bool:
return True
def _is_transient_stale_shaping_inputs_failure(exc: Exception) -> bool:
if not isinstance(exc, RefreshFailure):
return False
detail = str(exc or "").strip()
return detail.startswith("Missing or stale shaping_inputs.json.")
def _defer_stale_shaping_inputs_failure(exc: Exception, *, startup: bool) -> bool:
if not _is_transient_stale_shaping_inputs_failure(exc):
return False
ready, detail, generation = topology_runtime_readiness_detail()
if detail and not _topology_runtime_not_ready_is_transient(detail):
return False
clear_scheduler_error()
if startup:
_begin_startup_topology_runtime_wait(generation if not ready else current_topology_source_generation())
else:
_begin_partial_topology_runtime_wait(generation if not ready else current_topology_source_generation())
return True
def _publish_startup_topology_runtime_wait(generation: str | None):
global startup_topology_runtime_last_report_state
@@ -1349,6 +1379,8 @@ def _continue_startup_topology_runtime_wait():
shaping_runtime_hash = 0
return
except Exception as e:
if _defer_stale_shaping_inputs_failure(e, startup=True):
return
_reset_startup_topology_runtime_wait()
report_scheduler_runtime_failure(
"Scheduler startup shaping refresh failed",
@@ -1453,6 +1485,8 @@ def _continue_partial_topology_runtime_wait():
)
return
except Exception as e:
if _defer_stale_shaping_inputs_failure(e, startup=False):
return
_reset_partial_topology_runtime_wait()
report_scheduler_runtime_failure("Scheduled shaping refresh failed", e)
return
@@ -1546,6 +1580,8 @@ def topology_runtime_refresh_tick():
report_scheduler_validation_failure("Topology runtime refresh blocked by validation", e)
return
except Exception as e:
if _defer_stale_shaping_inputs_failure(e, startup=False):
return
report_scheduler_runtime_failure("Topology runtime refresh failed", e)
return
shaping_runtime_hash = calculate_shaping_runtime_hash()
+153
View File
@@ -22,8 +22,11 @@ scheduler = None
def install_scheduler_stubs():
libre = types.ModuleType("LibreQoS")
class RefreshFailure(Exception):
pass
class ValidationFailure(Exception):
pass
libre.RefreshFailure = RefreshFailure
libre.ValidationFailure = ValidationFailure
libre.refreshShapers = Mock()
libre.refreshShapersUpdateOnly = Mock()
@@ -377,6 +380,55 @@ class TestSchedulerLogging(unittest.TestCase):
"Topology runtime refresh failed: bad runtime"
)
def test_topology_runtime_refresh_tick_waits_for_stale_shaping_inputs(self):
stale_exc = scheduler.RefreshFailure(
"Missing or stale shaping_inputs.json. Run topology runtime before shaping."
)
with patch.object(scheduler, "ensure_topology_runtime_process"):
with patch.object(
scheduler,
"topology_runtime_readiness_detail",
return_value=(True, "", "generation-1"),
):
with patch.object(
scheduler,
"current_topology_source_generation",
return_value="generation-1",
):
with patch.object(scheduler, "calculate_shaping_runtime_hash", return_value=5):
with patch.object(
scheduler,
"refreshShapers",
side_effect=stale_exc,
):
with patch.object(scheduler, "clear_scheduler_error") as mock_clear_error:
with patch.object(
scheduler,
"report_scheduler_runtime_failure",
) as mock_report:
with patch.object(
scheduler,
"publish_scheduler_progress",
) as mock_progress:
with patch("builtins.print"):
scheduler.shaping_runtime_hash = 1
scheduler.topology_runtime_refresh_tick()
mock_clear_error.assert_called_once()
mock_report.assert_not_called()
self.assertTrue(scheduler.partial_topology_runtime_pending)
self.assertTrue(
any(
call.args[:3] == (
True,
"waiting_for_topology_runtime",
"Waiting for topology runtime",
)
for call in mock_progress.call_args_list
)
)
def test_topology_runtime_refresh_tick_reports_validation_failure(self):
validation_exc = scheduler.ValidationFailure("Validation failed. Will now exit.")
@@ -689,6 +741,59 @@ class TestSchedulerLogging(unittest.TestCase):
)
)
def test_partial_reload_waits_for_stale_shaping_inputs(self):
stale_exc = scheduler.RefreshFailure(
"Missing or stale shaping_inputs.json. Run topology runtime before shaping."
)
with patch.object(scheduler, "importFromCRM"):
with patch.object(scheduler, "ensure_topology_runtime_process", return_value=True):
with patch.object(scheduler, "calculate_shaping_runtime_hash", return_value=2):
with patch.object(scheduler, "shaping_runtime_hash", 1):
with patch.object(
scheduler,
"topology_runtime_readiness_detail",
return_value=(True, "", "generation-1"),
):
with patch.object(
scheduler,
"current_topology_source_generation",
return_value="generation-1",
):
with patch.object(
scheduler,
"refreshShapers",
side_effect=stale_exc,
):
with patch.object(
scheduler,
"clear_scheduler_error",
) as mock_clear_error:
with patch.object(
scheduler,
"report_scheduler_runtime_failure",
) as mock_report:
with patch.object(
scheduler,
"publish_scheduler_progress",
) as mock_progress:
with patch("builtins.print"):
scheduler.importAndShapePartialReload()
mock_clear_error.assert_called_once()
mock_report.assert_not_called()
self.assertTrue(scheduler.partial_topology_runtime_pending)
self.assertTrue(
any(
call.args[:3] == (
True,
"waiting_for_topology_runtime",
"Waiting for topology runtime",
)
for call in mock_progress.call_args_list
)
)
class TestTopologyRuntimeReadiness(unittest.TestCase):
def test_missing_status_is_not_ready(self):
@@ -898,6 +1003,54 @@ class TestTopologyRuntimeGating(unittest.TestCase):
mock_refresh.assert_not_called()
mock_report.assert_called_once()
def test_full_reload_waits_for_stale_shaping_inputs(self):
stale_exc = scheduler.RefreshFailure(
"Missing or stale shaping_inputs.json. Run topology runtime before shaping."
)
with patch.object(scheduler, "importFromCRM"):
with patch.object(scheduler, "ensure_topology_runtime_process", return_value=True):
with patch.object(scheduler, "enable_insight_topology", return_value=False):
with patch.object(
scheduler,
"topology_runtime_readiness_detail",
return_value=(True, "", "generation-1"),
):
with patch.object(
scheduler,
"current_topology_source_generation",
return_value="generation-1",
):
with patch.object(
scheduler,
"refreshShapers",
side_effect=stale_exc,
):
with patch.object(
scheduler,
"clear_scheduler_error",
) as mock_clear_error:
with patch.object(
scheduler,
"publish_scheduler_progress",
) as mock_progress:
with patch("builtins.print"):
ready = scheduler.importAndShapeFullReload()
self.assertFalse(ready)
mock_clear_error.assert_called_once()
self.assertTrue(scheduler.startup_topology_runtime_pending)
self.assertTrue(
any(
call.args[:3] == (
True,
"waiting_for_topology_runtime",
"Waiting for topology runtime",
)
for call in mock_progress.call_args_list
)
)
class TestSchedulerOverrideMerge(unittest.TestCase):
def test_merge_rows_replaces_matching_device_id(self):