Skip to content

Commit a8e4873

Browse files
authored
Merge pull request #113 from kormax/release-references
Fix _QThreadWorker.run not releasing references to fulfilled command and result objects before blocking on next queue.get call
2 parents de293ac + a5a21f9 commit a8e4873

File tree

2 files changed

+62
-0
lines changed

2 files changed

+62
-0
lines changed

qasync/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,15 @@ def run(self):
142142
else:
143143
self._logger.debug("Setting Future result: %s", r)
144144
future.set_result(r)
145+
finally:
146+
# Release potential reference
147+
r = None # noqa
145148
else:
146149
self._logger.debug("Future was canceled")
147150

151+
# Delete references
152+
del command, future, callback, args, kwargs
153+
148154
self._logger.debug("Thread #%s stopped", self.__num)
149155

150156
def wait(self):

tests/test_qthreadexec.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,33 @@
22
# © 2014 Mark Harviston <[email protected]>
33
# © 2014 Arve Knudsen <[email protected]>
44
# BSD License
5+
import logging
6+
import threading
7+
import weakref
58

69
import pytest
10+
711
import qasync
812

13+
_TestObject = type("_TestObject", (object,), {})
14+
15+
16+
@pytest.fixture
17+
def disable_executor_logging():
18+
"""
19+
When running under pytest, leftover LogRecord objects
20+
keep references to objects in the scope that logging was called in.
21+
To avoid issues with tests targeting stale references,
22+
we disable logging for QThreadExecutor and _QThreadWorker classes.
23+
"""
24+
for cls in (qasync.QThreadExecutor, qasync._QThreadWorker):
25+
logger_name = cls.__qualname__
26+
if cls.__module__ is not None:
27+
logger_name = f"{cls.__module__}.{logger_name}"
28+
logger = logging.getLogger(logger_name)
29+
logger.addHandler(logging.NullHandler())
30+
logger.propagate = False
31+
932

1033
@pytest.fixture
1134
def executor(request):
@@ -48,3 +71,36 @@ def rec(a, *args, **kwargs):
4871
for f in fs:
4972
with pytest.raises(RecursionError):
5073
f.result()
74+
75+
76+
def test_no_stale_reference_as_argument(executor, disable_executor_logging):
77+
test_obj = _TestObject()
78+
test_obj_collected = threading.Event()
79+
80+
# Reference to weakref has to be kept for callback to work
81+
_ = weakref.ref(test_obj, lambda *_: test_obj_collected.set())
82+
# Submit object as argument to the executor
83+
future = executor.submit(lambda *_: None, test_obj)
84+
del test_obj
85+
# Wait for future to resolve
86+
future.result()
87+
88+
collected = test_obj_collected.wait(timeout=1)
89+
assert collected is True, (
90+
"Stale reference to executor argument not collected within timeout."
91+
)
92+
93+
94+
def test_no_stale_reference_as_result(executor, disable_executor_logging):
95+
# Get object as result out of executor
96+
test_obj = executor.submit(lambda: _TestObject()).result()
97+
test_obj_collected = threading.Event()
98+
99+
# Reference to weakref has to be kept for callback to work
100+
_ = weakref.ref(test_obj, lambda *_: test_obj_collected.set())
101+
del test_obj
102+
103+
collected = test_obj_collected.wait(timeout=1)
104+
assert collected is True, (
105+
"Stale reference to executor result not collected within timeout."
106+
)

0 commit comments

Comments
 (0)