mirror of
https://github.com/LibreQoE/LibreQoS.git
synced 2026-10-25 22:30:05 -05:00
changes
This commit is contained in:
1 parent
ab7c9531ae
commit
392f122cd3
2 files changed
+191
-2
No files matched your search
+38
-2
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in new issue
Block a user