Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
3d481c8
`test_concurrent_futures.test_map_exception`: assert no reference cyc…
ebonnal Mar 24, 2025
f6f08a6
test there is no future in referrers
ebonnal Mar 25, 2025
a7a595d
switch to assertTrue(all(...))
ebonnal Mar 25, 2025
984d228
add test msg
ebonnal Mar 25, 2025
4cd4820
format msgs
ebonnal Mar 25, 2025
b7551c8
switch to assert list empty for a better assertion error message
ebonnal Mar 25, 2025
ec2b028
edit comment
ebonnal Mar 26, 2025
171e083
assert there are no reference cycles at all
graingert Mar 30, 2025
9710843
test that traceback frames should not contain any variables referring…
ebonnal Mar 30, 2025
00e7aee
interpreter.WorkerContext.run: finally unassign exc_wrapper
ebonnal Mar 30, 2025
c62b24e
fix msg grammar
ebonnal Mar 30, 2025
a3046c1
fix access to exception
ebonnal Mar 30, 2025
7b1021a
narrow test: traceback frames should not contain any exception
ebonnal Mar 30, 2025
e617e82
test_map_exception: only search for exception that captures itself in…
ebonnal Mar 31, 2025
f2e4dbe
concurrent.futures.process: avoid ref cycle in _sendback_result
ebonnal Mar 31, 2025
1245a9a
concurrent.futures.process: avoid ref cycle in _process_worker
ebonnal Mar 31, 2025
e984592
format test msg
ebonnal Mar 31, 2025
d3dee34
refactor ZeroDivisionError assert
ebonnal Mar 31, 2025
fc2a351
Revert "concurrent.futures.process: avoid ref cycle in _process_worker"
ebonnal Mar 31, 2025
2e29020
Revert "concurrent.futures.process: avoid ref cycle in _sendback_result"
ebonnal Mar 31, 2025
51269d9
skip referrers test for free-threading build on win/linux
ebonnal Mar 31, 2025
ed0e87f
call gc.collect for free-threading on linux/windows
ebonnal Mar 31, 2025
b3f3f4a
Revert "call gc.collect for free-threading on linux/windows"
ebonnal Apr 1, 2025
e3e75ce
Revert "skip referrers test for free-threading build on win/linux"
ebonnal Apr 1, 2025
70e22a1
more gc referrers
graingert Apr 3, 2025
12abc0e
even more gc referrers
graingert Apr 3, 2025
cac4aac
delete a ref to work_item
graingert Apr 4, 2025
66b98b4
delete reference to result_item before joining process
graingert Apr 4, 2025
68fcecb
unpack ResultItem and WorkItem
graingert Apr 4, 2025
0be8f49
fix typo
graingert Apr 5, 2025
6c2406d
bit of a hack?
graingert Apr 5, 2025
7037c4a
Revert investigations
ebonnal Apr 7, 2025
0568262
pause before checking referrers
ebonnal Apr 7, 2025
cfeaa9b
add comment about the avoided ref cycle with exc_wrapper
ebonnal Apr 7, 2025
346b4ea
format test
ebonnal Apr 7, 2025
2a25b9a
make the self-captured exceptions check not rely on var name
ebonnal Apr 7, 2025
fd9b9bb
use traceback.walk_tb
ebonnal Apr 7, 2025
682a719
comment the skipping of the current frame
ebonnal Apr 7, 2025
fb0c522
test_map_exception: improve readability
ebonnal Apr 23, 2025
e0a031d
Fix CI: break exception ref cycle in _MapResultIterator.__next__ (gh-…
ambv Oct 21, 2026
6c8f6d6
test_map_exception: wait for transient referrers instead of sleep(1)
ambv Oct 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Lib/concurrent/futures/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,11 @@ def __iter__(self):
def __next__(self):
value, exc = next(self.gen)
if exc is not None:
raise exc
try:
raise exc
finally:
# avoid a ref cycle where exc is captured in its traceback
exc = None
return value

def close(self):
Expand Down
6 changes: 5 additions & 1 deletion Lib/concurrent/futures/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,11 @@ def run(self, task):
if exc is None:
# The exception must have been not shareable.
raise # re-raise
raise exc from wrapper
try:
raise exc from wrapper
finally:
# avoid a ref cycle where exc is captured in its traceback
exc = None


class BrokenInterpreterPool(_thread.BrokenThreadPool):
Expand Down
43 changes: 41 additions & 2 deletions Lib/test/test_concurrent_futures/executor.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import gc
import itertools
import threading
import time
import traceback
import weakref
from concurrent import futures
from operator import add
Expand Down Expand Up @@ -80,7 +82,44 @@ def test_map_exception(self):
i = self.executor.map(divmod, [5, 5, 5, 5], [2, 3, 0, 5])
self.assertEqual(next(i), (2, 1))
self.assertEqual(next(i), (1, 2))
self.assertRaises(ZeroDivisionError, next, i)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test looks fragile to me, +1 to remove test altogether, the fix is straightforward enough

exception = None
try:
i.__next__()
except Exception as e:
exception = e
self.assertTrue(
isinstance(exception, ZeroDivisionError),
msg="should raise a ZeroDivisionError",
)

# Wait for the completing thread to release its transient reference
# to the exception; do not keep the referrers list alive in the loop.
for _ in support.sleeping_retry(support.SHORT_TIMEOUT, error=False):
if not gc.get_referrers(exception):
break

self.assertFalse(
gc.get_referrers(exception),
msg="the exception should not have any referrers",
)

self.assertFalse(
[
(var, val)
# go through the frames of the exception's traceback
for frame, _ in traceback.walk_tb(exception.__traceback__)
# skipping the current frame
if frame is not exception.__traceback__.tb_frame
# go through the locals captured in that frame
for var, val in frame.f_locals.items()
# check if one of them is an exception
if isinstance(val, Exception)
# check if it is captured in its own traceback
and frame is val.__traceback__.tb_frame
],
msg=f"the exception's traceback should not contain an exception captured in its own traceback",
)
self.assertEqual(next(i), (1, 0))
self.assertRaises(StopIteration, next, i)
self.assertRaises(StopIteration, next, i)
Expand Down Expand Up @@ -212,7 +251,7 @@ def test_map_buffersize_when_buffer_is_full(self):
self.assertEqual(
next(ints),
buffersize,
msg="should have fetched only `buffersize` elements from `ints`.",
msg="should have fetched only `buffersize` elements from `ints`",
)

@warnings_helper.ignore_fork_in_thread_deprecation_warnings()
Expand Down
Loading