mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 14:25:08 +00:00
fix(display): a raising display() counts as a circuit-breaker failure (#707)
The first-frame dispatch (_dispatch_first_frame) now asks PluginExecutor.execute_display() to re-raise (raise_errors=True) and records a raise inside the executor as a breaker failure, with the original exception as last_error, instead of a success. The screen is still an empty pass and rotation is unchanged; a hung display() is still recorded once, as a hang. The run-loop golden trace plugin_error.json is regenerated (crashy now records health failures and is skipped by the breaker), and behaviour 7 is dropped from docs/RUN_LOOP_REDESIGN.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,7 @@ from src.config_service import ConfigService
|
||||
from src.cache_manager import CacheManager
|
||||
from src.font_manager import FontManager
|
||||
from src.logging_config import get_logger
|
||||
from src.exceptions import PluginError
|
||||
from src.common.sync_manager import DisplaySyncManager, SyncRole
|
||||
from src.ipc.server import ControlServer, start_control_server
|
||||
from src.vegas_mode.render_pipeline import SYNC_SEND_INTERVAL
|
||||
@@ -2499,6 +2500,8 @@ class DisplayController:
|
||||
display_lock = pm.get_plugin_lock(plugin_id) if pm else None
|
||||
can_display = display_lock is None or display_lock.acquire(blocking=False)
|
||||
display_hung = False
|
||||
# Set when display() raised inside the executor.
|
||||
display_error: Optional[Exception] = None
|
||||
|
||||
if display_lock is None:
|
||||
# Only when plugin loading failed part-way.
|
||||
@@ -2556,8 +2559,23 @@ class DisplayController:
|
||||
# the SimpleNamespace built above -- a
|
||||
# fresh callable every call, so nothing
|
||||
# there can ever cache.
|
||||
accepts_display_mode=_accepts_display_mode
|
||||
accepts_display_mode=_accepts_display_mode,
|
||||
# A raise must reach the breaker as a
|
||||
# failure. As a bare False it read as
|
||||
# "no content" and was recorded as a
|
||||
# success, so the breaker never tripped
|
||||
# on a plugin that raises every time.
|
||||
raise_errors=True
|
||||
)
|
||||
except PluginError as exc:
|
||||
# display() raised. The executor has logged
|
||||
# and recorded it, and _display_target's
|
||||
# finally released the lock. The screen is
|
||||
# an empty pass, as before; only the health
|
||||
# record changes. Keep what display() raised
|
||||
# as last_error, not the executor's wrapper.
|
||||
display_error = exc.__cause__ or exc
|
||||
result = False
|
||||
except Exception: # pragma: no cover - defensive;
|
||||
# execute_display catches everything
|
||||
# internally, but guarantee the lock is
|
||||
@@ -2589,10 +2607,15 @@ class DisplayController:
|
||||
# frame, not a real success, and must not clear
|
||||
# force_change or the pending mode-switch clear will
|
||||
# be lost when display() finally does run.
|
||||
# A hang was already recorded as a failure by
|
||||
# record_display_hang, so it records nothing here.
|
||||
if can_display:
|
||||
health_tracker = self._health_tracker()
|
||||
if health_tracker is not None and not display_hung:
|
||||
health_tracker.record_success(plugin_id)
|
||||
if display_error is not None:
|
||||
health_tracker.record_failure(plugin_id, display_error)
|
||||
else:
|
||||
health_tracker.record_success(plugin_id)
|
||||
self.force_change = False
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
logger.exception("Error displaying %s", self.current_display_mode)
|
||||
|
||||
@@ -173,7 +173,8 @@ class PluginExecutor:
|
||||
force_clear: bool = False,
|
||||
display_mode: Optional[str] = None,
|
||||
timeout: Optional[float] = None,
|
||||
accepts_display_mode: Optional[bool] = None
|
||||
accepts_display_mode: Optional[bool] = None,
|
||||
raise_errors: bool = False
|
||||
) -> bool:
|
||||
"""
|
||||
Execute plugin display() method with error handling.
|
||||
@@ -187,9 +188,18 @@ class PluginExecutor:
|
||||
accepts_display_mode: Whether plugin.display() takes a
|
||||
display_mode keyword. Pass it when the caller already knows;
|
||||
None falls back to inspecting the callable.
|
||||
raise_errors: Re-raise the PluginError wrapping an exception
|
||||
display() raised, instead of returning False. False alone
|
||||
cannot tell "no content" from "raised", and a caller that
|
||||
feeds the circuit breaker needs that difference. The error
|
||||
is still logged and recorded first. A timeout still returns
|
||||
False either way.
|
||||
|
||||
Returns:
|
||||
True if display succeeded, False otherwise
|
||||
|
||||
Raises:
|
||||
PluginError: Only with ``raise_errors``, when display() raised.
|
||||
"""
|
||||
try:
|
||||
start_time = time.monotonic()
|
||||
@@ -245,6 +255,8 @@ class PluginExecutor:
|
||||
return False
|
||||
except PluginError:
|
||||
# Already logged and recorded in execute_with_timeout
|
||||
if raise_errors:
|
||||
raise
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
|
||||
Reference in New Issue
Block a user