@@ -1101,32 +1101,91 @@ async def branch(index: int) -> None:
11011101 assert cleaned == [0 , 1 ]
11021102
11031103
1104+ @pytest .mark .parametrize ("count" , [2 , 32 , 6000 ])
11041105def test_cleanup_closes_async_generators_started_during_generator_cleanup (
1105- caplog : pytest .LogCaptureFixture ) -> None :
1106- closed : list [str ] = []
1106+ count : int , caplog : pytest .LogCaptureFixture ) -> None :
1107+ closed : list [int ] = []
11071108
11081109 async def orchestrator (ctx : task .OrchestrationContext , _ : Any ) -> None :
1109- async def child ( ) -> AsyncIterator [int ]:
1110+ async def resource ( index : int ) -> AsyncIterator [int ]:
11101111 try :
11111112 yield 1
11121113 finally :
1114+ if index + 1 < count :
1115+ await anext (resource (index + 1 ))
11131116 await asyncio .sleep (0 )
1114- closed .append ("child" )
1115-
1116- async def outer () -> AsyncIterator [int ]:
1117- try :
1118- yield 1
1119- finally :
1120- await anext (child ())
1121- closed .append ("outer" )
1117+ closed .append (index )
11221118
1123- await anext (outer ( ))
1119+ await anext (resource ( 0 ))
11241120 await ctx .call_activity ("wait" )
11251121
11261122 actions = _execute (orchestrator , [], _start ()).actions
11271123 assert [action .scheduleTask .name for action in actions ] == ["wait" ]
1128- assert closed == ["outer" , "child" ]
1129- assert "Async" not in caplog .text
1124+ assert closed == list (range (count ))
1125+ if count == 6000 :
1126+ assert "Async cleanup did not quiesce" in caplog .text
1127+ else :
1128+ assert "Async" not in caplog .text
1129+
1130+
1131+ @pytest .mark .parametrize ("control" , [None , "system_exit" , "keyboard_interrupt" , "nested_group" ])
1132+ def test_infinite_asyncgen_discovery_has_bounded_cleanup (control : str | None ) -> None :
1133+ code = """
1134+ import asyncio
1135+ import logging
1136+ import sys
1137+ from durabletask import task
1138+ from tests.durabletask.test_asyncio_orchestrators import _cleanup_signal
1139+ from tests.durabletask.test_coroutine_orchestrators import _execute, _start
1140+
1141+ control = sys.argv[1]
1142+ exit_error = None if control == "none" else _cleanup_signal(control)
1143+ warnings = []
1144+
1145+ class Capture(logging.Handler):
1146+ def emit(self, record):
1147+ warnings.append(logging.Formatter().format(record))
1148+
1149+ logging.getLogger().addHandler(Capture())
1150+
1151+ async def orchestrator(ctx: task.OrchestrationContext, _):
1152+ async def resource():
1153+ try:
1154+ yield 1
1155+ finally:
1156+ await anext(resource())
1157+
1158+ async def signal_on_forced_close():
1159+ try:
1160+ await ctx.wait_for_external_event("never")
1161+ finally:
1162+ try:
1163+ while True:
1164+ try:
1165+ await asyncio.Future()
1166+ except asyncio.CancelledError:
1167+ pass
1168+ finally:
1169+ raise exit_error
1170+
1171+ if exit_error is not None:
1172+ asyncio.create_task(signal_on_forced_close())
1173+ await anext(resource())
1174+ await ctx.call_activity("wait")
1175+
1176+ try:
1177+ result = _execute(orchestrator, [], _start())
1178+ except BaseException as error:
1179+ assert exit_error is not None and error is exit_error, repr(error)
1180+ else:
1181+ assert exit_error is None
1182+ assert [action.scheduleTask.name for action in result.actions] == ["wait"]
1183+ assert any("Async cleanup did not quiesce" in message for message in warnings), warnings
1184+ """
1185+ process = subprocess .run (
1186+ [sys .executable , "-c" , code , control or "none" ],
1187+ capture_output = True , text = True , timeout = 30 )
1188+ assert process .returncode == 0 , process .stdout + process .stderr
11301189
11311190
11321191def test_exhausted_async_generators_are_not_retained_until_dispatch_end () -> None :
@@ -1146,7 +1205,10 @@ async def values() -> AsyncIterator[int]:
11461205 assert sum (reference () is not None for reference in references ) < 100
11471206 await ctx .call_activity ("wait" )
11481207
1149- _execute (orchestrator , [], _start ())
1208+ actions = _execute (orchestrator , [], _start ()).actions
1209+ assert len (actions ) == 1
1210+ assert actions [0 ].HasField ("scheduleTask" )
1211+ assert actions [0 ].scheduleTask .name == "wait"
11501212
11511213
11521214@pytest .mark .parametrize ("suspend" , ["activity" , "entity" ])
0 commit comments