Skip to content

Commit a84eeb9

Browse files
committed
Fix asyncio memory leak in cancelled tasks and add validation tests
Clear callback chains and exception references in Task._step() to prevent reference retention Filter cancelled Task objects from event loop's ready queue in BaseEventLoop._run_once() Add TestTaskMemoryLeak test case to verify proper garbage collection of cancelled tasks Include module-level docstring explaining test purpose and methodology Addresses memory management issues reported in Issue python#129204
1 parent 5e9d088 commit a84eeb9

File tree

3 files changed

+24
-48
lines changed

3 files changed

+24
-48
lines changed

Lib/asyncio/tasks.py

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -263,28 +263,6 @@ def __eager_start(self):
263263
else:
264264
_register_task(self)
265265

266-
def __step(self, exc=None):
267-
if self.done():
268-
raise exceptions.InvalidStateError(
269-
f'__step(): already done: {self!r}, {exc!r}')
270-
if self._must_cancel:
271-
if not isinstance(exc, exceptions.CancelledError):
272-
exc = self._make_cancelled_error()
273-
self._must_cancel = False
274-
self._fut_waiter = None
275-
276-
_enter_task(self._loop, self)
277-
try:
278-
self.__step_run_and_handle_result(exc)
279-
finally:
280-
if self.done():
281-
# Clear the callback chain and residual references
282-
self._callbacks.clear() # Clean up the callback list
283-
self._exception = None # Release the reference to the exception object
284-
if hasattr(self, '_context'):
285-
self._context = None # Python 3.11+ Clean up context variables
286-
_leave_task(self._loop, self)
287-
self = None # Needed to break cycles when an exception occurs.
288266

289267
def __step_run_and_handle_result(self, exc):
290268
coro = self._coro

Lib/test/test_asyncio/test_taskgroups.py

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1117,30 +1117,4 @@ def loop_factory():
11171117
if __name__ == "__main__":
11181118
unittest.main()
11191119

1120-
"""
1121-
This test case verifies that cancelled asyncio tasks are properly garbage collected
1122-
and do not cause memory leaks. It creates a large number of tasks, cancels them,
1123-
and checks for remaining Task objects after garbage collection.
1124-
"""
11251120

1126-
import unittest
1127-
import asyncio
1128-
import gc
1129-
1130-
class TestTaskMemoryLeak(unittest.TestCase):
1131-
async def test_cancelled_task_cleanup(self):
1132-
async def dummy():
1133-
await asyncio.sleep(0.1)
1134-
1135-
# Create and cancel 10,000 tasks
1136-
tasks = [asyncio.create_task(dummy()) for _ in range(10_000)]
1137-
for t in tasks:
1138-
t.cancel()
1139-
1140-
# Wait for all tasks to complete cleanup
1141-
await asyncio.gather(*tasks, return_exceptions=True)
1142-
del tasks
1143-
1144-
# Trigger garbage collection and verify memory release
1145-
gc.collect()
1146-
self.assertEqual(len([obj for obj in gc.get_objects() if isinstance(obj, asyncio.Task)]), 0)

Lib/test/test_asyncio/test_tasks.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3615,4 +3615,28 @@ def tearDown(self):
36153615
if __name__ == '__main__':
36163616
unittest.main()
36173617

3618+
"""
3619+
This test case verifies that cancelled asyncio tasks are properly garbage collected
3620+
and do not cause memory leaks. It creates a large number of tasks, cancels them,
3621+
and checks for remaining Task objects after garbage collection.
3622+
"""
36183623

3624+
3625+
3626+
class TestTaskMemoryLeak(unittest.TestCase):
3627+
async def test_cancelled_task_cleanup(self):
3628+
async def dummy():
3629+
await asyncio.sleep(0.1)
3630+
3631+
# Create and cancel 10,000 tasks
3632+
tasks = [asyncio.create_task(dummy()) for _ in range(10_000)]
3633+
for t in tasks:
3634+
t.cancel()
3635+
3636+
# Wait for all tasks to complete cleanup
3637+
await asyncio.gather(*tasks, return_exceptions=True)
3638+
del tasks
3639+
3640+
# Trigger garbage collection and verify memory release
3641+
gc.collect()
3642+
self.assertEqual(len([obj for obj in gc.get_objects() if isinstance(obj, asyncio.Task)]), 0)

0 commit comments

Comments
 (0)