From 268bd0d8ecac10c06506ccf3a308360a856b9cbd Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Wed, 26 Jun 2024 11:44:31 -0400 Subject: [PATCH] Demo-abandonment on shielded `trio`-side work Finally this reproduces the issue as it (originally?) exhibited inside `piker` where the `Actor.lifetime_stack` wasn't closed in cases where during `infected_aio`-actor cancellation/shutdown `trio` side tasks which are doing shielded (teardown) work are NOT being watched/waited on from the `aio_main()` task-closure inside `run_as_asyncio_guest()`! This is then the root cause of the guest-run being abandoned since if our `aio_main()` task-closure doesn't know it should allow the run to finish, it's going to call `loop.close()` eventually resulting in the `GeneratorExit` thrown into `trio._core._run.unrolled_run()`.. So, this extends the `test_sigint_closes_lifetime_stack()` suite to include cases for such shielded `trio`-task ops: - add a new `trio_side_is_shielded: bool` which will toggle whether to add a shielded 0.5s `trio.sleep()` loop to `manage_file()` which should outlive the `asyncio` event-loop shutdown sequence and result in an abandoned guest-run and thus a leaked file. - parametrize the existing suite with this case resulting in a total 16 test set B) This patch demonstrates the problem with our `aio_main()` task-closure impl via the now 4 failing tests, a fix is coming in a follow up commit! --- tests/test_infected_asyncio.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/tests/test_infected_asyncio.py b/tests/test_infected_asyncio.py index 645dc4b..42eb35b 100644 --- a/tests/test_infected_asyncio.py +++ b/tests/test_infected_asyncio.py @@ -644,6 +644,7 @@ async def manage_file( ctx: tractor.Context, tmp_path_str: str, send_sigint_to: str, + trio_side_is_shielded: bool = True, bg_aio_task: bool = False, ): ''' @@ -693,11 +694,6 @@ async def manage_file( # => ????? honestly i'm lost but it seems to be some issue # with `asyncio` and SIGINT.. # - # XXX NOTE XXX SO, if this LINE IS UNCOMMENTED and - # `run_as_asyncio_guest()` is written WITHOUT THE - # `.cancel_soon()` soln, both of these tests will pass ?? - # so maybe it has something to do with `asyncio` loop init - # state?!? # honestly, this REALLY reminds me why i haven't used # `asyncio` by choice in years.. XD # @@ -715,6 +711,15 @@ async def manage_file( # os.getpid(), # signal.SIGINT, # ) + + # XXX spend a half sec doing shielded checkpointing to + # ensure that despite the `trio`-side task ignoring the + # SIGINT, the `asyncio` side won't abandon the guest-run! + if trio_side_is_shielded: + with trio.CancelScope(shield=True): + for i in range(5): + await trio.sleep(0.1) + await trio.sleep_forever() # signalled manually at the OS level (aka KBI) by the parent actor. @@ -726,6 +731,17 @@ async def manage_file( raise RuntimeError('shoulda received a KBI?') +@pytest.mark.parametrize( + 'trio_side_is_shielded', + [ + False, + True, + ], + ids=[ + 'trio_side_no_shielding', + 'trio_side_does_shielded_work', + ], +) @pytest.mark.parametrize( 'send_sigint_to', [ @@ -768,6 +784,7 @@ def test_sigint_closes_lifetime_stack( tmp_path: Path, wait_for_ctx: bool, bg_aio_task: bool, + trio_side_is_shielded: bool, debug_mode: bool, send_sigint_to: str, ): @@ -793,6 +810,7 @@ def test_sigint_closes_lifetime_stack( tmp_path_str=str(tmp_path), send_sigint_to=send_sigint_to, bg_aio_task=bg_aio_task, + trio_side_is_shielded=trio_side_is_shielded, ) as (ctx, first): path_str, cpid = first