fix(rpc): isolate broadcast callbacks and retry delivery after failures

This commit is contained in:
2026-07-24 19:49:57 +02:00
parent 201a173202
commit 8e340acfc5
2 changed files with 42 additions and 1 deletions
+11 -1
View File
@@ -182,6 +182,7 @@ class RPCRegister:
self._dirty = False
connections = self.list_all_connections()
dead_refs = []
delivery_failed = False
for callback_ref in list(self.callbacks):
callback = callback_ref()
if callback is None:
@@ -193,12 +194,21 @@ class RPCRegister:
# e.g. during shutdown): calling it would raise on any Qt access.
dead_refs.append(callback_ref)
continue
callback(connections)
try:
callback(connections)
except Exception:
delivery_failed = True
logger.exception(f"RPC registry broadcast callback {callback!r} failed")
for ref in dead_refs:
try:
self.callbacks.remove(ref)
except ValueError:
pass
if delivery_failed:
# The state change must not be lost: leave the registry dirty so the
# next broadcast retries delivery (duplicates are safe, the payload
# is the full state).
self._dirty = True
def object_is_registered(self, obj: BECConnector) -> bool:
"""
+31
View File
@@ -189,3 +189,34 @@ def test_broadcast_without_callbacks_skips_registry_walk(rpc_register, monkeypat
rpc_register.broadcast()
walk.assert_called_once()
assert len(received) == 1
def test_broadcast_prunes_callbacks_of_destroyed_qobjects(rpc_register, qapp):
"""A bound-method callback whose QObject was destroyed (Python wrapper still
referenced, C++ side gone — the shutdown scenario) must be pruned, not called."""
import shiboken6
from qtpy.QtCore import QObject
class Listener(QObject):
def __init__(self):
super().__init__()
self.received = []
def on_update(self, connections):
self.received.append(connections)
listener = Listener()
rpc_register.add_callback(listener.on_update)
rpc_register.mark_dirty()
rpc_register.broadcast()
assert len(listener.received) == 1
callbacks_before = len(rpc_register.callbacks)
shiboken6.delete(listener)
assert not shiboken6.isValid(listener)
rpc_register.mark_dirty()
rpc_register.broadcast() # must neither call the dead callback nor raise
assert len(listener.received) == 1
assert len(rpc_register.callbacks) == callbacks_before - 1