diff --git a/Lib/concurrent/futures/interpreter.py b/Lib/concurrent/futures/interpreter.py index d17688dc9d7346..030c36ec9b31f1 100644 --- a/Lib/concurrent/futures/interpreter.py +++ b/Lib/concurrent/futures/interpreter.py @@ -205,8 +205,11 @@ def run(self, task): assert res is None, res assert pickled assert exc_wrapper is not None - exc = pickle.loads(excdata) - raise exc from exc_wrapper + try: + raise pickle.loads(excdata) from exc_wrapper + finally: + # avoid a ref cycle where exc_wrapper is captured in its traceback + exc_wrapper = None return pickle.loads(res) if pickled else res diff --git a/Lib/test/test_concurrent_futures/executor.py b/Lib/test/test_concurrent_futures/executor.py index d88c34d1c8c8e4..6cdd8f4fd61537 100644 --- a/Lib/test/test_concurrent_futures/executor.py +++ b/Lib/test/test_concurrent_futures/executor.py @@ -1,6 +1,8 @@ +import gc import itertools import threading import time +import traceback import weakref from concurrent import futures from operator import add @@ -55,8 +57,41 @@ def test_map_exception(self): i = self.executor.map(divmod, [1, 1, 1, 1], [2, 3, 0, 5]) self.assertEqual(i.__next__(), (0, 1)) self.assertEqual(i.__next__(), (0, 1)) - with self.assertRaises(ZeroDivisionError): + + exception = None + try: i.__next__() + except Exception as e: + exception = e + self.assertTrue( + isinstance(exception, ZeroDivisionError), + msg="should raise a ZeroDivisionError", + ) + + # pause needed for free-threading builds on Ubuntu (ARM) and Windows + time.sleep(1) + + 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", + ) @support.requires_resource('walltime') def test_map_timeout(self): @@ -140,7 +175,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`", ) def test_shutdown_race_issue12456(self):