fix(connector): break C++-anchored reference cycle retaining task owners

This commit is contained in:
2026-08-04 22:27:51 +02:00
committed by Jan Wyzula
parent a55c543e16
commit 115b0cf07a
2 changed files with 34 additions and 1 deletions
+11 -1
View File
@@ -403,8 +403,18 @@ class BECConnector:
# Keep a reference to the worker so it is not garbage collected.
self._workers.append(worker)
# When the worker is done (success or failure), remove it from our list.
# When the worker is done (success or failure), remove it from our
# list. The closure must disconnect itself from both signals before
# returning: the Qt connection holds the closure strongly from C++,
# and the closure captures self and worker — without the disconnect
# this forms a reference cycle anchored in C++ that Python's GC
# cannot break, keeping the owning widget alive forever.
def _discard_worker(*_):
for signal in (worker.signals.completed, worker.signals.failed):
try:
signal.disconnect(_discard_worker)
except (RuntimeError, TypeError):
pass
try:
self._workers.remove(worker)
except ValueError:
+23
View File
@@ -277,3 +277,26 @@ def test_bec_connector_parent_id_returns_none_on_error(bec_connector):
bec_connector, "_get_rpc_parent_ancestor", side_effect=ValueError("broken hierarchy")
):
assert bec_connector.parent_id is None
def test_bec_connector_worker_completion_does_not_retain_owner(qtbot, mocked_client):
"""The worker-discard closure is held strongly
by the Qt signal connection and captures the owner; without disconnecting
itself it forms a C++-anchored reference cycle that keeps the owner (and
widget) alive forever."""
import gc
import weakref
connector = _CleanupBroadcastWidget(client=mocked_client)
connector.submit_task(lambda: None)
qtbot.waitUntil(lambda: not connector._workers, timeout=5000)
ref = weakref.ref(connector)
connector.close()
connector.deleteLater()
qtbot.wait(20)
del connector
for _ in range(3):
gc.collect()
assert ref() is None, "connector kept alive by worker completion closure"