mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 13:41:24 +00:00 
			
		
		
		
	gh-128552: fix refcycles in eager task creation (#128553)
(cherry picked from commit 61b9811ac6)
			
			
This commit is contained in:
		
							parent
							
								
									7e099c51b6
								
							
						
					
					
						commit
						13835888e6
					
				
					 4 changed files with 72 additions and 6 deletions
				
			
		|  | @ -477,7 +477,12 @@ def create_task(self, coro, *, name=None, context=None): | ||||||
| 
 | 
 | ||||||
|             task.set_name(name) |             task.set_name(name) | ||||||
| 
 | 
 | ||||||
|         return task |         try: | ||||||
|  |             return task | ||||||
|  |         finally: | ||||||
|  |             # gh-128552: prevent a refcycle of | ||||||
|  |             # task.exception().__traceback__->BaseEventLoop.create_task->task | ||||||
|  |             del task | ||||||
| 
 | 
 | ||||||
|     def set_task_factory(self, factory): |     def set_task_factory(self, factory): | ||||||
|         """Set a task factory that will be used by loop.create_task(). |         """Set a task factory that will be used by loop.create_task(). | ||||||
|  |  | ||||||
|  | @ -205,7 +205,12 @@ def create_task(self, coro, *, name=None, context=None): | ||||||
|         else: |         else: | ||||||
|             self._tasks.add(task) |             self._tasks.add(task) | ||||||
|             task.add_done_callback(self._on_task_done) |             task.add_done_callback(self._on_task_done) | ||||||
|         return task |         try: | ||||||
|  |             return task | ||||||
|  |         finally: | ||||||
|  |             # gh-128552: prevent a refcycle of | ||||||
|  |             # task.exception().__traceback__->TaskGroup.create_task->task | ||||||
|  |             del task | ||||||
| 
 | 
 | ||||||
|     # Since Python 3.8 Tasks propagate all exceptions correctly, |     # Since Python 3.8 Tasks propagate all exceptions correctly, | ||||||
|     # except for KeyboardInterrupt and SystemExit which are |     # except for KeyboardInterrupt and SystemExit which are | ||||||
|  |  | ||||||
|  | @ -1,6 +1,8 @@ | ||||||
| # Adapted with permission from the EdgeDB project; | # Adapted with permission from the EdgeDB project; | ||||||
| # license: PSFL. | # license: PSFL. | ||||||
| 
 | 
 | ||||||
|  | import weakref | ||||||
|  | import sys | ||||||
| import gc | import gc | ||||||
| import asyncio | import asyncio | ||||||
| import contextvars | import contextvars | ||||||
|  | @ -28,7 +30,25 @@ def get_error_types(eg): | ||||||
|     return {type(exc) for exc in eg.exceptions} |     return {type(exc) for exc in eg.exceptions} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TestTaskGroup(unittest.IsolatedAsyncioTestCase): | def set_gc_state(enabled): | ||||||
|  |     was_enabled = gc.isenabled() | ||||||
|  |     if enabled: | ||||||
|  |         gc.enable() | ||||||
|  |     else: | ||||||
|  |         gc.disable() | ||||||
|  |     return was_enabled | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @contextlib.contextmanager | ||||||
|  | def disable_gc(): | ||||||
|  |     was_enabled = set_gc_state(enabled=False) | ||||||
|  |     try: | ||||||
|  |         yield | ||||||
|  |     finally: | ||||||
|  |         set_gc_state(enabled=was_enabled) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class BaseTestTaskGroup: | ||||||
| 
 | 
 | ||||||
|     async def test_taskgroup_01(self): |     async def test_taskgroup_01(self): | ||||||
| 
 | 
 | ||||||
|  | @ -822,15 +842,15 @@ async def test_taskgroup_without_parent_task(self): | ||||||
|         with self.assertRaisesRegex(RuntimeError, "has not been entered"): |         with self.assertRaisesRegex(RuntimeError, "has not been entered"): | ||||||
|             tg.create_task(coro) |             tg.create_task(coro) | ||||||
| 
 | 
 | ||||||
|     def test_coro_closed_when_tg_closed(self): |     async def test_coro_closed_when_tg_closed(self): | ||||||
|         async def run_coro_after_tg_closes(): |         async def run_coro_after_tg_closes(): | ||||||
|             async with taskgroups.TaskGroup() as tg: |             async with taskgroups.TaskGroup() as tg: | ||||||
|                 pass |                 pass | ||||||
|             coro = asyncio.sleep(0) |             coro = asyncio.sleep(0) | ||||||
|             with self.assertRaisesRegex(RuntimeError, "is finished"): |             with self.assertRaisesRegex(RuntimeError, "is finished"): | ||||||
|                 tg.create_task(coro) |                 tg.create_task(coro) | ||||||
|         loop = asyncio.get_event_loop() | 
 | ||||||
|         loop.run_until_complete(run_coro_after_tg_closes()) |         await run_coro_after_tg_closes() | ||||||
| 
 | 
 | ||||||
|     async def test_cancelling_level_preserved(self): |     async def test_cancelling_level_preserved(self): | ||||||
|         async def raise_after(t, e): |         async def raise_after(t, e): | ||||||
|  | @ -955,6 +975,30 @@ async def coro_fn(): | ||||||
|         self.assertIsInstance(exc, _Done) |         self.assertIsInstance(exc, _Done) | ||||||
|         self.assertListEqual(gc.get_referrers(exc), []) |         self.assertListEqual(gc.get_referrers(exc), []) | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  |     async def test_exception_refcycles_parent_task_wr(self): | ||||||
|  |         """Test that TaskGroup deletes self._parent_task and create_task() deletes task""" | ||||||
|  |         tg = asyncio.TaskGroup() | ||||||
|  |         exc = None | ||||||
|  | 
 | ||||||
|  |         class _Done(Exception): | ||||||
|  |             pass | ||||||
|  | 
 | ||||||
|  |         async def coro_fn(): | ||||||
|  |             async with tg: | ||||||
|  |                 raise _Done | ||||||
|  | 
 | ||||||
|  |         with disable_gc(): | ||||||
|  |             try: | ||||||
|  |                 async with asyncio.TaskGroup() as tg2: | ||||||
|  |                     task_wr = weakref.ref(tg2.create_task(coro_fn())) | ||||||
|  |             except* _Done as excs: | ||||||
|  |                 exc = excs.exceptions[0].exceptions[0] | ||||||
|  | 
 | ||||||
|  |         self.assertIsNone(task_wr()) | ||||||
|  |         self.assertIsInstance(exc, _Done) | ||||||
|  |         self.assertListEqual(gc.get_referrers(exc), []) | ||||||
|  | 
 | ||||||
|     async def test_exception_refcycles_propagate_cancellation_error(self): |     async def test_exception_refcycles_propagate_cancellation_error(self): | ||||||
|         """Test that TaskGroup deletes propagate_cancellation_error""" |         """Test that TaskGroup deletes propagate_cancellation_error""" | ||||||
|         tg = asyncio.TaskGroup() |         tg = asyncio.TaskGroup() | ||||||
|  | @ -988,5 +1032,16 @@ class MyKeyboardInterrupt(KeyboardInterrupt): | ||||||
|         self.assertListEqual(gc.get_referrers(exc), []) |         self.assertListEqual(gc.get_referrers(exc), []) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | class TestTaskGroup(BaseTestTaskGroup, unittest.IsolatedAsyncioTestCase): | ||||||
|  |     loop_factory = asyncio.EventLoop | ||||||
|  | 
 | ||||||
|  | class TestEagerTaskTaskGroup(BaseTestTaskGroup, unittest.IsolatedAsyncioTestCase): | ||||||
|  |     @staticmethod | ||||||
|  |     def loop_factory(): | ||||||
|  |         loop = asyncio.EventLoop() | ||||||
|  |         loop.set_task_factory(asyncio.eager_task_factory) | ||||||
|  |         return loop | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
|     unittest.main() |     unittest.main() | ||||||
|  |  | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | Fix cyclic garbage introduced by :meth:`asyncio.loop.create_task` and :meth:`asyncio.TaskGroup.create_task` holding a reference to the created task if it is eager. | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Thomas Grainger
						Thomas Grainger