mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-08-10 04:00:36 +02:00
fix(logpanel): keep the log queue out of the RPC registry and tear it down with the last panel
This commit is contained in:
@@ -170,9 +170,13 @@ class BecLogsQueue(BECConnector, QObject):
|
||||
def __init__(self, parent: QObject | None, maxlen: int = 2500, **kwargs) -> None:
|
||||
if BecLogsQueue._instance:
|
||||
raise RuntimeError("Create no more than one BecLogsQueue - use BecLogsQueue.instance()")
|
||||
super().__init__(parent=parent, **kwargs)
|
||||
# rpc_exposed=False: a background data pump with no RPC surface must not enter
|
||||
# the RPC registry, where it would linger as a windowless connection
|
||||
super().__init__(parent=parent, rpc_exposed=False, **kwargs)
|
||||
self._max_length = maxlen
|
||||
self._paused = False
|
||||
self._attached_panels = 0
|
||||
self._cleaned = False
|
||||
self._data = deque(
|
||||
(
|
||||
item["data"]
|
||||
@@ -209,13 +213,30 @@ class BecLogsQueue(BECConnector, QObject):
|
||||
self._paused = not self._paused
|
||||
self.paused.emit(self._paused)
|
||||
|
||||
def attach(self):
|
||||
"""Register a consumer panel; the queue stays alive while any panel is open."""
|
||||
self._attached_panels += 1
|
||||
|
||||
def detach(self):
|
||||
"""Unregister a consumer panel. The last one tears the queue down - log history
|
||||
survives in Redis and is re-read when the next panel opens."""
|
||||
self._attached_panels -= 1
|
||||
if self._attached_panels <= 0:
|
||||
self.cleanup()
|
||||
|
||||
def cleanup(self, *_):
|
||||
"""Stop listening to the Redis log stream"""
|
||||
"""Stop listening to the Redis log stream and release the singleton. Idempotent:
|
||||
reachable from the last panel's detach, from tests, and from app quit."""
|
||||
if self._cleaned:
|
||||
return
|
||||
self._cleaned = True
|
||||
QCoreApplication.instance().aboutToQuit.disconnect(self.cleanup)
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self._process_incoming_log_msg, [MessageEndpoints.log()]
|
||||
)
|
||||
self._update_timer.stop()
|
||||
BecLogsQueue._instance = None
|
||||
self.deleteLater()
|
||||
|
||||
@SafeSlot(verify_sender=True)
|
||||
def _process_incoming_log_msg(self, msg: dict, _metadata: dict):
|
||||
@@ -244,6 +265,7 @@ class BecLogsTableModel(QAbstractTableModel):
|
||||
def __init__(self, parent: QWidget | None = None):
|
||||
super().__init__(parent)
|
||||
self.log_queue = BecLogsQueue.instance()
|
||||
self.log_queue.attach()
|
||||
self._headers = _CONST.headers
|
||||
self._max_length = self.log_queue.max_length
|
||||
self._rows: list[_LogRec] = self.log_queue.snapshot_records()
|
||||
@@ -975,11 +997,13 @@ class LogPanel(BECWidget, QWidget):
|
||||
return QSize(600, 300)
|
||||
|
||||
def cleanup(self):
|
||||
"""Detach from the shared log queue so a closed panel stops receiving updates."""
|
||||
"""Detach from the shared log queue so a closed panel stops receiving updates.
|
||||
The last panel's detach tears the queue down entirely."""
|
||||
self._model.log_queue.new_records.disconnect(self._model._on_new_records)
|
||||
if hasattr(self, "_toolbar"):
|
||||
self._model.log_queue.paused.disconnect(self._toolbar.set_paused)
|
||||
self._model.log_queue.buffered.disconnect(self._toolbar.set_buffered)
|
||||
self._model.log_queue.detach()
|
||||
super().cleanup()
|
||||
|
||||
|
||||
|
||||
@@ -273,12 +273,41 @@ def test_log_panel_survives_malformed_messages(qtbot, log_panel: LogPanel):
|
||||
second_panel.close()
|
||||
|
||||
|
||||
def test_log_panel_close_detaches_from_queue(qtbot, log_panel: LogPanel):
|
||||
def test_log_panel_close_tears_down_queue(qtbot, log_panel: LogPanel):
|
||||
from bec_widgets.widgets.utility.logpanel.logpanel import BecLogsQueue
|
||||
|
||||
queue = log_panel._model.log_queue
|
||||
assert not queue.rpc_register.object_is_registered(queue) # never an RPC connection
|
||||
log_panel.close()
|
||||
# the last panel's close fully tears the queue down - nothing lingers after it
|
||||
assert BecLogsQueue._instance is None
|
||||
assert not queue._update_timer.isActive()
|
||||
_feed(log_panel, [make_log_msg(0)])
|
||||
assert log_panel._model.rowCount() == 3 # closed panel no longer receives updates
|
||||
assert len(queue) == 4 # the shared history still ingests
|
||||
|
||||
|
||||
def test_log_panel_queue_survives_until_last_panel_closes(qtbot, mocked_client, monkeypatch):
|
||||
from bec_widgets.widgets.utility.logpanel.logpanel import BecLogsQueue
|
||||
|
||||
monkeypatch.setattr(mocked_client.connector, "xread", lambda *_, **__: TEST_LOG_MESSAGES)
|
||||
first = LogPanel()
|
||||
qtbot.addWidget(first)
|
||||
second = LogPanel()
|
||||
qtbot.addWidget(second)
|
||||
queue = first._model.log_queue
|
||||
assert second._model.log_queue is queue
|
||||
second.close()
|
||||
assert BecLogsQueue._instance is queue # one panel still open
|
||||
assert queue._update_timer.isActive()
|
||||
first.close()
|
||||
assert BecLogsQueue._instance is None
|
||||
assert not queue._update_timer.isActive()
|
||||
# a new panel re-creates the queue and backfills its history from Redis
|
||||
reopened = LogPanel()
|
||||
qtbot.addWidget(reopened)
|
||||
assert reopened._model.log_queue is not queue
|
||||
assert reopened._model.rowCount() == 3
|
||||
reopened.close()
|
||||
|
||||
|
||||
def test_direct_queue_construction_registers_singleton(qtbot, mocked_client, monkeypatch):
|
||||
|
||||
Reference in New Issue
Block a user