Much more limited `asyncio.Task.cancel()` use

Since it can not only cause the guest-mode run to abandon but also in
some edge cases prevent `trio`-errors from propagating (at least on
py3.12-13?) as discovered as part of supporting this mode officially
in the *root actor*.

As such try to avoid that method as much as possible instead opting to
pass the `trio`-side error via the iter-task channel ref.

Deats,
- add a `LinkedTaskChannel._trio_err: BaseException|None` which gets set
  whenver the `trio.Task` error is caught; ONLY set `AsyncioCancelled`
  when the `trio` task was for sure the cause, whether itself cancelled
  or errored.
- always check for this error when exiting the `asyncio` side (even when
  terminated via a call to `asyncio.Task.cancel()` or during any other
  `CancelledError` handling such that the `asyncio`-task can expect to
  handle `AsyncioCancelled` due to the above^^ cases.
- never `cs.cancel()` the `trio` side unless that cancel scope has not
  yet been `.cancel_called` whatsoever; it's a noop anyway.
- only raise any exc from `asyncio.Task.result()` when `chan._aio_err`
  does not already match it since the existence of the pre-existing
  `task_err` means `asyncio` prolly intends (or has already) raised and
  interrupted the task elsewhere.

Various supporting tweaks,
- don't bother maybe-init-ing `greenback` from the actor entrypoint
  since we already need to (and do) bestow the portals to each `asyncio`
  task spawned using the `run_task()`/`open_channel_from()` API; further
  the init-ing should be done already by client code that enables
  infected mode (even in the root actor).
 |_we should prolly also codify it from any
   `run_daemon(infected_aio=True, debug_mode=True)` usage we offer.
- pass all the `_<field>`s to `Linked TaskChannel` explicitly in named
  kwarg style.
- better sclang-style log reports throughout, particularly on teardowns.
- generally more/better comments and docs around (not well understood)
  edge cases.
- prep to just inline `maybe_raise_aio_side_err()` closure..
hilevel_serman
Tyler Goodlet 2024-12-31 18:10:09 -05:00
parent c63b94f61f
commit 7b8a8dcc7c
1 changed files with 331 additions and 140 deletions

View File

@ -33,13 +33,19 @@ from typing import (
) )
import tractor import tractor
from tractor._exceptions import AsyncioCancelled from tractor._exceptions import (
AsyncioCancelled,
is_multi_cancelled,
)
from tractor._state import ( from tractor._state import (
debug_mode, debug_mode,
_runtime_vars, _runtime_vars,
) )
from tractor.devx import _debug from tractor.devx import _debug
from tractor.log import get_logger from tractor.log import (
get_logger,
StackLevelAdapter,
)
from tractor.trionics._broadcast import ( from tractor.trionics._broadcast import (
broadcast_receiver, broadcast_receiver,
BroadcastReceiver, BroadcastReceiver,
@ -50,7 +56,7 @@ from outcome import (
Outcome, Outcome,
) )
log = get_logger(__name__) log: StackLevelAdapter = get_logger(__name__)
__all__ = [ __all__ = [
@ -70,9 +76,10 @@ class LinkedTaskChannel(trio.abc.Channel):
_to_aio: asyncio.Queue _to_aio: asyncio.Queue
_from_aio: trio.MemoryReceiveChannel _from_aio: trio.MemoryReceiveChannel
_to_trio: trio.MemorySendChannel _to_trio: trio.MemorySendChannel
_trio_cs: trio.CancelScope _trio_cs: trio.CancelScope
_aio_task_complete: trio.Event _aio_task_complete: trio.Event
_trio_err: BaseException|None = None
_trio_exited: bool = False _trio_exited: bool = False
# set after ``asyncio.create_task()`` # set after ``asyncio.create_task()``
@ -84,28 +91,40 @@ class LinkedTaskChannel(trio.abc.Channel):
await self._from_aio.aclose() await self._from_aio.aclose()
async def receive(self) -> Any: async def receive(self) -> Any:
async with translate_aio_errors( '''
self, Receive a value from the paired `asyncio.Task` with
exception/cancel handling to teardown both sides on any
# XXX: obviously this will deadlock if an on-going stream is unexpected error.
# being procesed.
# wait_on_aio_task=False,
):
'''
try:
# TODO: do we need this to guarantee asyncio code get's # TODO: do we need this to guarantee asyncio code get's
# cancelled in the case where the trio side somehow creates # cancelled in the case where the trio side somehow creates
# a state where the asyncio cycle-task isn't getting the # a state where the asyncio cycle-task isn't getting the
# cancel request sent by (in theory) the last checkpoint # cancel request sent by (in theory) the last checkpoint
# cycle on the trio side? # cycle on the trio side?
# await trio.lowlevel.checkpoint() # await trio.lowlevel.checkpoint()
return await self._from_aio.receive() return await self._from_aio.receive()
except BaseException as err:
async with translate_aio_errors(
self,
# XXX: obviously this will deadlock if an on-going stream is
# being procesed.
# wait_on_aio_task=False,
):
raise err
async def wait_asyncio_complete(self) -> None: async def wait_asyncio_complete(self) -> None:
await self._aio_task_complete.wait() await self._aio_task_complete.wait()
# def cancel_asyncio_task(self) -> None: def cancel_asyncio_task(
# self._aio_task.cancel() self,
msg: str = '',
) -> None:
self._aio_task.cancel(
msg=msg,
)
async def send(self, item: Any) -> None: async def send(self, item: Any) -> None:
''' '''
@ -155,7 +174,6 @@ class LinkedTaskChannel(trio.abc.Channel):
def _run_asyncio_task( def _run_asyncio_task(
func: Callable, func: Callable,
*, *,
qsize: int = 1, qsize: int = 1,
@ -165,8 +183,9 @@ def _run_asyncio_task(
) -> LinkedTaskChannel: ) -> LinkedTaskChannel:
''' '''
Run an ``asyncio`` async function or generator in a task, return Run an `asyncio`-compat async function or generator in a task,
or stream the result back to the caller `trio.lowleve.Task`. return or stream the result back to the caller
`trio.lowleve.Task`.
''' '''
__tracebackhide__: bool = hide_tb __tracebackhide__: bool = hide_tb
@ -204,23 +223,23 @@ def _run_asyncio_task(
aio_err: BaseException|None = None aio_err: BaseException|None = None
chan = LinkedTaskChannel( chan = LinkedTaskChannel(
aio_q, # asyncio.Queue _to_aio=aio_q, # asyncio.Queue
from_aio, # recv chan _from_aio=from_aio, # recv chan
to_trio, # send chan _to_trio=to_trio, # send chan
_trio_cs=cancel_scope,
cancel_scope, _aio_task_complete=aio_task_complete,
aio_task_complete,
) )
async def wait_on_coro_final_result( async def wait_on_coro_final_result(
to_trio: trio.MemorySendChannel, to_trio: trio.MemorySendChannel,
coro: Awaitable, coro: Awaitable,
aio_task_complete: trio.Event, aio_task_complete: trio.Event,
) -> None: ) -> None:
''' '''
Await ``coro`` and relay result back to ``trio``. Await `coro` and relay result back to `trio`.
This can only be run as an `asyncio.Task`!
''' '''
nonlocal aio_err nonlocal aio_err
@ -243,8 +262,10 @@ def _run_asyncio_task(
else: else:
if ( if (
result != orig and result != orig
aio_err is None and and
aio_err is None
and
# in the `open_channel_from()` case we don't # in the `open_channel_from()` case we don't
# relay through the "return value". # relay through the "return value".
@ -260,12 +281,21 @@ def _run_asyncio_task(
# a ``trio.EndOfChannel`` to the trio (consumer) side. # a ``trio.EndOfChannel`` to the trio (consumer) side.
to_trio.close() to_trio.close()
# import pdbp; pdbp.set_trace()
aio_task_complete.set() aio_task_complete.set()
log.runtime(f'`asyncio` task: {task.get_name()} is complete') # await asyncio.sleep(0.1)
log.info(
f'`asyncio` task terminated\n'
f'x)>\n'
f' |_{task}\n'
)
# start the asyncio task we submitted from trio # start the asyncio task we submitted from trio
if not inspect.isawaitable(coro): if not inspect.isawaitable(coro):
raise TypeError(f"No support for invoking {coro}") raise TypeError(
f'Pass the async-fn NOT a coroutine\n'
f'{coro!r}'
)
task: asyncio.Task = asyncio.create_task( task: asyncio.Task = asyncio.create_task(
wait_on_coro_final_result( wait_on_coro_final_result(
@ -289,6 +319,10 @@ def _run_asyncio_task(
raise_not_found=False, raise_not_found=False,
)) ))
): ):
log.info(
f'Bestowing `greenback` portal for `asyncio`-task\n'
f'{task}\n'
)
greenback.bestow_portal(task) greenback.bestow_portal(task)
def cancel_trio(task: asyncio.Task) -> None: def cancel_trio(task: asyncio.Task) -> None:
@ -304,11 +338,22 @@ def _run_asyncio_task(
# task exceptions # task exceptions
try: try:
res: Any = task.result() res: Any = task.result()
log.info(
'`trio` received final result from {task}\n'
f'|_{res}\n'
)
except BaseException as terr: except BaseException as terr:
task_err: BaseException = terr task_err: BaseException = terr
# read again AFTER the `asyncio` side errors in case
# it was cancelled due to an error from `trio` (or
# some other out of band exc).
aio_err: BaseException|None = chan._aio_err
msg: str = ( msg: str = (
'Infected `asyncio` task {etype_str}\n' '`trio`-side reports that the `asyncio`-side '
'{etype_str}\n'
# ^NOTE filled in below
) )
if isinstance(terr, CancelledError): if isinstance(terr, CancelledError):
msg += ( msg += (
@ -327,17 +372,18 @@ def _run_asyncio_task(
msg.format(etype_str='errored') msg.format(etype_str='errored')
) )
assert type(terr) is type(aio_err), ( assert (
'`asyncio` task error mismatch?!?' type(terr) is type(aio_err)
) ), '`asyncio` task error mismatch?!?'
if aio_err is not None: if aio_err is not None:
# import pdbp; pdbp.set_trace()
# XXX: uhh is this true? # XXX: uhh is this true?
# assert task_err, f'Asyncio task {task.get_name()} discrepancy!?' # assert task_err, f'Asyncio task {task.get_name()} discrepancy!?'
# NOTE: currently mem chan closure may act as a form # NOTE: currently mem chan closure may act as a form
# of error relay (at least in the ``asyncio.CancelledError`` # of error relay (at least in the `asyncio.CancelledError`
# case) since we have no way to directly trigger a ``trio`` # case) since we have no way to directly trigger a `trio`
# task error without creating a nursery to throw one. # task error without creating a nursery to throw one.
# We might want to change this in the future though. # We might want to change this in the future though.
from_aio.close() from_aio.close()
@ -359,29 +405,25 @@ def _run_asyncio_task(
# ) # )
# raise aio_err from task_err # raise aio_err from task_err
# XXX: if not already, alway cancel the scope # XXX: if not already, alway cancel the scope on a task
# on a task error in case the trio task is blocking on # error in case the trio task is blocking on
# a checkpoint. # a checkpoint.
cancel_scope.cancel()
if ( if (
task_err not cancel_scope.cancelled_caught
and or
aio_err is not task_err not cancel_scope.cancel_called
): ):
raise aio_err from task_err # import pdbp; pdbp.set_trace()
cancel_scope.cancel()
# raise any `asyncio` side error. if task_err:
raise aio_err # XXX raise any `asyncio` side error IFF it doesn't
# match the one we just caught from the task above!
log.info( # (that would indicate something weird/very-wrong
'`trio` received final result from {task}\n' # going on?)
f'|_{res}\n' if aio_err is not task_err:
) # import pdbp; pdbp.set_trace()
# TODO: do we need this? raise aio_err from task_err
# if task_err:
# cancel_scope.cancel()
# raise task_err
task.add_done_callback(cancel_trio) task.add_done_callback(cancel_trio)
return chan return chan
@ -389,13 +431,18 @@ def _run_asyncio_task(
@acm @acm
async def translate_aio_errors( async def translate_aio_errors(
chan: LinkedTaskChannel, chan: LinkedTaskChannel,
wait_on_aio_task: bool = False, wait_on_aio_task: bool = False,
cancel_aio_task_on_trio_exit: bool = True,
) -> AsyncIterator[None]: ) -> AsyncIterator[None]:
''' '''
Error handling context around ``asyncio`` task spawns which An error handling to cross-loop propagation context around
`asyncio.Task` spawns via one of this module's APIs:
- `open_channel_from()`
- `run_task()`
appropriately translates errors and cancels into ``trio`` land. appropriately translates errors and cancels into ``trio`` land.
''' '''
@ -403,88 +450,204 @@ async def translate_aio_errors(
aio_err: BaseException|None = None aio_err: BaseException|None = None
# TODO: make thisi a channel method? aio_task: asyncio.Task = chan._aio_task
def maybe_raise_aio_err( assert aio_task
err: Exception|None = None trio_err: BaseException|None = None
) -> None:
aio_err = chan._aio_err
if (
aio_err is not None
and
# not isinstance(aio_err, CancelledError)
type(aio_err) != CancelledError
):
# always raise from any captured asyncio error
if err:
raise aio_err from err
else:
raise aio_err
task = chan._aio_task
assert task
try: try:
yield yield # back to one of the cross-loop apis
except ( except (
trio.Cancelled, trio.Cancelled,
): ) as _trio_err:
# relay cancel through to called ``asyncio`` task trio_err = _trio_err
assert chan._aio_task assert chan._aio_task
chan._aio_task.cancel(
msg=f'the `trio` caller task was cancelled: {trio_task.name}' # import pdbp; pdbp.set_trace() # lolevel-debug
# relay cancel through to called ``asyncio`` task
chan._aio_err = AsyncioCancelled(
f'trio`-side cancelled the `asyncio`-side,\n'
f'c)>\n'
f' |_{trio_task}\n\n'
f'{trio_err!r}\n'
) )
raise
# XXX NOTE XXX seems like we can get all sorts of unreliable
# behaviour from `asyncio` under various cancellation
# conditions (like SIGINT/kbi) when this is used..
# SO FOR NOW, try to avoid it at most costs!
#
# aio_task.cancel(
# msg=f'the `trio` parent task was cancelled: {trio_task.name}'
# )
# raise
except ( except (
# NOTE: see the note in the ``cancel_trio()`` asyncio task # NOTE: also see note in the `cancel_trio()` asyncio task
# termination callback # termination callback
trio.ClosedResourceError, trio.ClosedResourceError,
# trio.BrokenResourceError, # trio.BrokenResourceError,
):
) as _trio_err:
trio_err = _trio_err
aio_err = chan._aio_err aio_err = chan._aio_err
# import pdbp; pdbp.set_trace()
# XXX if an underlying `asyncio.CancelledError` triggered
# this channel close, raise our (non-`BaseException`) wrapper
# exception (`AsyncioCancelled`) from that source error.
if ( if (
task.cancelled() # NOTE, not until it terminates?
aio_task.cancelled()
and and
type(aio_err) is CancelledError type(aio_err) is CancelledError
): ):
# if an underlying `asyncio.CancelledError` triggered this
# channel close, raise our (non-``BaseException``) wrapper
# error: ``AsyncioCancelled`` from that source error.
raise AsyncioCancelled( raise AsyncioCancelled(
f'Task cancelled\n' f'asyncio`-side cancelled the `trio`-side,\n'
f'|_{task}\n' f'c(>\n'
f' |_{aio_task}\n\n'
f'{trio_err!r}\n'
) from aio_err ) from aio_err
else: else:
raise raise
finally: except BaseException as _trio_err:
trio_err = _trio_err
log.exception(
'`trio`-side task errored?'
)
entered: bool = await _debug._maybe_enter_pm(
trio_err,
api_frame=inspect.currentframe(),
)
if ( if (
# NOTE: always cancel the ``asyncio`` task if we've made it not entered
# this far and it's not done. and
not task.done() and aio_err not is_multi_cancelled(trio_err)
):
log.exception('actor crashed\n')
aio_taskc = AsyncioCancelled(
f'`trio`-side task errored!\n'
f'{trio_err}'
) #from trio_err
try:
aio_task.set_exception(aio_taskc)
except (
asyncio.InvalidStateError,
RuntimeError,
# ^XXX, uhh bc apparently we can't use `.set_exception()`
# any more XD .. ??
):
wait_on_aio_task = False
# import pdbp; pdbp.set_trace()
# raise aio_taskc from trio_err
finally:
# record wtv `trio`-side error transpired
chan._trio_err = trio_err
# NOTE! by default always cancel the `asyncio` task if
# we've made it this far and it's not done.
# TODO, how to detect if there's an out-of-band error that
# caused the exit?
if (
cancel_aio_task_on_trio_exit
and
not aio_task.done()
and
aio_err
# or the trio side has exited it's surrounding cancel scope # or the trio side has exited it's surrounding cancel scope
# indicating the lifetime of the ``asyncio``-side task # indicating the lifetime of the ``asyncio``-side task
# should also be terminated. # should also be terminated.
or chan._trio_exited or (
): chan._trio_exited
log.runtime( and
f'Cancelling `asyncio`-task: {task.get_name()}' not chan._trio_err # XXX CRITICAL, `asyncio.Task.cancel()` is cucked man..
) )
# assert not aio_err, 'WTF how did asyncio do this?!' ):
task.cancel() # pass
msg: str = (
f'MANUALLY Cancelling `asyncio`-task: {aio_task.get_name()}!\n\n'
f'**THIS CAN SILENTLY SUPPRESS ERRORS FYI\n\n'
# Required to sync with the far end ``asyncio``-task to ensure f'trio-side exited silently!'
)
# TODO XXX, figure out the case where calling this makes the
# `test_infected_asyncio.py::test_trio_closes_early_and_channel_exits`
# hang and then don't call it in that case!
#
aio_task.cancel(msg=msg)
log.warning(msg)
# assert not aio_err, 'WTF how did asyncio do this?!'
# import pdbp; pdbp.set_trace()
# Required to sync with the far end `asyncio`-task to ensure
# any error is captured (via monkeypatching the # any error is captured (via monkeypatching the
# ``channel._aio_err``) before calling ``maybe_raise_aio_err()`` # `channel._aio_err`) before calling ``maybe_raise_aio_err()``
# below! # below!
#
# XXX NOTE XXX the `task.set_exception(aio_taskc)` call above
# MUST NOT EXCEPT or this WILL HANG!!
#
# so if you get a hang maybe step through and figure out why
# it erroed out up there!
#
if wait_on_aio_task: if wait_on_aio_task:
# await chan.wait_asyncio_complete()
await chan._aio_task_complete.wait() await chan._aio_task_complete.wait()
log.info(
'asyncio-task is done and unblocked trio-side!\n'
)
# TODO?
# -[ ] make this a channel method, OR
# -[ ] just put back inline below?
#
def maybe_raise_aio_side_err(
trio_err: Exception,
) -> None:
'''
Raise any `trio`-side-caused cancellation or legit task
error normally propagated from the caller of either,
- `open_channel_from()`
- `run_task()`
'''
aio_err: BaseException|None = chan._aio_err
# Check if the asyncio-side is the cause of the trio-side
# error.
if (
aio_err is not None
and
type(aio_err) is not AsyncioCancelled
# not isinstance(aio_err, CancelledError)
# type(aio_err) is not CancelledError
):
# always raise from any captured asyncio error
if trio_err:
raise trio_err from aio_err
raise aio_err
if trio_err:
raise trio_err
# NOTE: if any ``asyncio`` error was caught, raise it here inline # NOTE: if any ``asyncio`` error was caught, raise it here inline
# here in the ``trio`` task # here in the ``trio`` task
maybe_raise_aio_err() # if trio_err:
maybe_raise_aio_side_err(
trio_err=trio_err
)
async def run_task( async def run_task(
@ -496,8 +659,8 @@ async def run_task(
) -> Any: ) -> Any:
''' '''
Run an `asyncio` async function or generator in a task, return Run an `asyncio`-compat async function or generator in a task,
or stream the result back to `trio`. return or stream the result back to `trio`.
''' '''
# simple async func # simple async func
@ -537,6 +700,7 @@ async def open_channel_from(
provide_channels=True, provide_channels=True,
**kwargs, **kwargs,
) )
# TODO, tuple form here?
async with chan._from_aio: async with chan._from_aio:
async with translate_aio_errors( async with translate_aio_errors(
chan, chan,
@ -685,18 +849,21 @@ def run_as_asyncio_guest(
# Uh, oh. # Uh, oh.
# #
# :o # :o
#
# It looks like your event loop has caught a case of the ``trio``s. # looks like your stdlib event loop has caught a case of "the trios" !
#
# :() # :O
#
# Don't worry, we've heard you'll barely notice. You might # Don't worry, we've heard you'll barely notice.
# hallucinate a few more propagating errors and feel like your #
# digestion has slowed but if anything get's too bad your parents
# will know about it.
# :) # :)
#
# You might hallucinate a few more propagating errors and feel
# like your digestion has slowed, but if anything get's too bad
# your parents will know about it.
#
# B)
#
async def aio_main(trio_main): async def aio_main(trio_main):
''' '''
Main `asyncio.Task` which calls Main `asyncio.Task` which calls
@ -713,16 +880,20 @@ def run_as_asyncio_guest(
'-> built a `trio`-done future\n' '-> built a `trio`-done future\n'
) )
# TODO: shoudn't this be done in the guest-run trio task? # TODO: is this evern run or needed?
# if debug_mode(): # -[ ] pretty sure it never gets run for root-infected-aio
# # XXX make it obvi we know this isn't supported yet! # since this main task is always the parent of any
# log.error( # eventual `open_root_actor()` call?
# 'Attempting to enter unsupported `greenback` init ' if debug_mode():
# 'from `asyncio` task..' log.error(
# ) 'Attempting to enter non-required `greenback` init '
# await _debug.maybe_init_greenback( 'from `asyncio` task ???'
# force_reload=True, )
# ) # XXX make it obvi we know this isn't supported yet!
assert 0
# await _debug.maybe_init_greenback(
# force_reload=True,
# )
def trio_done_callback(main_outcome): def trio_done_callback(main_outcome):
log.runtime( log.runtime(
@ -732,6 +903,7 @@ def run_as_asyncio_guest(
) )
if isinstance(main_outcome, Error): if isinstance(main_outcome, Error):
# import pdbp; pdbp.set_trace()
error: BaseException = main_outcome.error error: BaseException = main_outcome.error
# show an dedicated `asyncio`-side tb from the error # show an dedicated `asyncio`-side tb from the error
@ -751,7 +923,7 @@ def run_as_asyncio_guest(
trio_done_fute.set_result(main_outcome) trio_done_fute.set_result(main_outcome)
log.info( log.info(
f'`trio` guest-run finished with outcome\n' f'`trio` guest-run finished with,\n'
f')>\n' f')>\n'
f'|_{trio_done_fute}\n' f'|_{trio_done_fute}\n'
) )
@ -777,9 +949,20 @@ def run_as_asyncio_guest(
done_callback=trio_done_callback, done_callback=trio_done_callback,
) )
fute_err: BaseException|None = None fute_err: BaseException|None = None
try: try:
out: Outcome = await asyncio.shield(trio_done_fute) out: Outcome = await asyncio.shield(trio_done_fute)
# ^TODO still don't really understand why the `.shield()`
# is required ... ??
# https://docs.python.org/3/library/asyncio-task.html#asyncio.shield
# ^ seems as though in combo with the try/except here
# we're BOLDLY INGORING cancel of the trio fute?
#
# I guess it makes sense bc we don't want `asyncio` to
# cancel trio just because they can't handle SIGINT
# sanely? XD .. kk
# XXX, sin-shield causes guest-run abandons on SIGINT..
# out: Outcome = await trio_done_fute
# NOTE will raise (via `Error.unwrap()`) from any # NOTE will raise (via `Error.unwrap()`) from any
# exception packed into the guest-run's `main_outcome`. # exception packed into the guest-run's `main_outcome`.
@ -802,27 +985,32 @@ def run_as_asyncio_guest(
fute_err = _fute_err fute_err = _fute_err
err_message: str = ( err_message: str = (
'main `asyncio` task ' 'main `asyncio` task '
'was cancelled!\n'
) )
if isinstance(fute_err, asyncio.CancelledError):
err_message += 'was cancelled!\n'
else:
err_message += f'errored with {out.error!r}\n'
# TODO, handle possible edge cases with
# `open_root_actor()` closing before this is run!
#
actor: tractor.Actor = tractor.current_actor() actor: tractor.Actor = tractor.current_actor()
log.exception( log.exception(
err_message err_message
+ +
'Cancelling `trio`-side `tractor`-runtime..\n' 'Cancelling `trio`-side `tractor`-runtime..\n'
f'c)>\n' f'c(>\n'
f' |_{actor}.cancel_soon()\n' f' |_{actor}.cancel_soon()\n'
) )
# XXX WARNING XXX the next LOCs are super important, since # XXX WARNING XXX the next LOCs are super important!
# without them, we can get guest-run abandonment cases #
# where `asyncio` will not schedule or wait on the `trio` # SINCE without them, we can get guest-run ABANDONMENT
# guest-run task before final shutdown! This is # cases where `asyncio` will not schedule or wait on the
# particularly true if the `trio` side has tasks doing # guest-run `trio.Task` nor invoke its registered
# shielded work when a SIGINT condition occurs. # `trio_done_callback()` before final shutdown!
#
# This is particularly true if the `trio` side has tasks
# in shielded sections when an OC-cancel (SIGINT)
# condition occurs!
# #
# We now have the # We now have the
# `test_infected_asyncio.test_sigint_closes_lifetime_stack()` # `test_infected_asyncio.test_sigint_closes_lifetime_stack()`
@ -886,7 +1074,10 @@ def run_as_asyncio_guest(
try: try:
return trio_done_fute.result() return trio_done_fute.result()
except asyncio.exceptions.InvalidStateError as state_err: except (
asyncio.InvalidStateError,
# asyncio.CancelledError,
)as state_err:
# XXX be super dupere noisy about abandonment issues! # XXX be super dupere noisy about abandonment issues!
aio_task: asyncio.Task = asyncio.current_task() aio_task: asyncio.Task = asyncio.current_task()