From 8e4cfee427dc9e4a020f742f1512b4b242a46013 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Fri, 8 Mar 2024 23:27:41 +0200 Subject: [PATCH 1/3] Fix _QThreadWorker.run not releasing references to fulfilled command and result objects before blocking on next queue.get call --- qasync/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/qasync/__init__.py b/qasync/__init__.py index 732f7a2..25c57d2 100644 --- a/qasync/__init__.py +++ b/qasync/__init__.py @@ -142,9 +142,15 @@ def run(self): else: self._logger.debug("Setting Future result: %s", r) future.set_result(r) + finally: + # Release potential reference + r = None # noqa else: self._logger.debug("Future was canceled") + # Delete references + del command, future, callback, args, kwargs + self._logger.debug("Thread #%s stopped", self.__num) def wait(self): From cca1617b646fe609fdb370bfd8d63bcb64726ba9 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Wed, 23 Jul 2025 23:23:16 +0300 Subject: [PATCH 2/3] Add tests for _QThreadWorker.run dangling reference release --- tests/test_qthreadexec.py | 57 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/test_qthreadexec.py b/tests/test_qthreadexec.py index d09b7f6..0691535 100644 --- a/tests/test_qthreadexec.py +++ b/tests/test_qthreadexec.py @@ -2,11 +2,35 @@ # © 2014 Mark Harviston # © 2014 Arve Knudsen # BSD License +import threading +import weakref +import logging import pytest + import qasync +_TestObject = type("_TestObject", (object,), {}) + + +@pytest.fixture(autouse=True) +def disable_executor_logging(): + """ + When running under pytest, leftover LogRecord objects + keep references to objects in the scope that logging was called in. + To avoid issues with tests targeting stale references, + we disable logging for QThreadExecutor and _QThreadWorker classes. + """ + for cls in (qasync.QThreadExecutor, qasync._QThreadWorker): + logger_name = cls.__qualname__ + if cls.__module__ is not None: + logger_name = f"{cls.__module__}.{logger_name}" + logger = logging.getLogger(logger_name) + logger.addHandler(logging.NullHandler()) + logger.propagate = False + + @pytest.fixture def executor(request): exe = qasync.QThreadExecutor(5) @@ -48,3 +72,36 @@ def rec(a, *args, **kwargs): for f in fs: with pytest.raises(RecursionError): f.result() + + +def test_no_stale_reference_as_argument(executor): + test_obj = _TestObject() + test_obj_collected = threading.Event() + + # Reference to weakref has to be kept for callback to work + _ = weakref.ref(test_obj, lambda *_: test_obj_collected.set()) + # Submit object as argument to the executor + future = executor.submit(lambda *_: None, test_obj) + del test_obj + # Wait for future to resolve + future.result() + + collected = test_obj_collected.wait(timeout=1) + assert ( + collected is True + ), "Stale reference to executor argument not collected within timeout." + + +def test_no_stale_reference_as_result(executor): + # Get object as result out of executor + test_obj = executor.submit(lambda: _TestObject()).result() + test_obj_collected = threading.Event() + + # Reference to weakref has to be kept for callback to work + _ = weakref.ref(test_obj, lambda *_: test_obj_collected.set()) + del test_obj + + collected = test_obj_collected.wait(timeout=1) + assert ( + collected is True + ), "Stale reference to executor result not collected within timeout." From a5a21f9d3208a487f296c6e0ab6cdf33f1861d74 Mon Sep 17 00:00:00 2001 From: Alex March Date: Thu, 24 Jul 2025 12:38:45 +0900 Subject: [PATCH 3/3] Explicitly use disable_executor_loggin fixture --- tests/test_qthreadexec.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/tests/test_qthreadexec.py b/tests/test_qthreadexec.py index 0691535..67c1833 100644 --- a/tests/test_qthreadexec.py +++ b/tests/test_qthreadexec.py @@ -2,19 +2,18 @@ # © 2014 Mark Harviston # © 2014 Arve Knudsen # BSD License +import logging import threading import weakref -import logging import pytest import qasync - _TestObject = type("_TestObject", (object,), {}) -@pytest.fixture(autouse=True) +@pytest.fixture def disable_executor_logging(): """ When running under pytest, leftover LogRecord objects @@ -74,7 +73,7 @@ def rec(a, *args, **kwargs): f.result() -def test_no_stale_reference_as_argument(executor): +def test_no_stale_reference_as_argument(executor, disable_executor_logging): test_obj = _TestObject() test_obj_collected = threading.Event() @@ -87,12 +86,12 @@ def test_no_stale_reference_as_argument(executor): future.result() collected = test_obj_collected.wait(timeout=1) - assert ( - collected is True - ), "Stale reference to executor argument not collected within timeout." + assert collected is True, ( + "Stale reference to executor argument not collected within timeout." + ) -def test_no_stale_reference_as_result(executor): +def test_no_stale_reference_as_result(executor, disable_executor_logging): # Get object as result out of executor test_obj = executor.submit(lambda: _TestObject()).result() test_obj_collected = threading.Event() @@ -102,6 +101,6 @@ def test_no_stale_reference_as_result(executor): del test_obj collected = test_obj_collected.wait(timeout=1) - assert ( - collected is True - ), "Stale reference to executor result not collected within timeout." + assert collected is True, ( + "Stale reference to executor result not collected within timeout." + )