Skip to content

Commit

Permalink
Merge pull request #128 from oremanj/fix-startup-deadlock
Browse files Browse the repository at this point in the history
Fix potential deadlock if open_loop() is cancelled
  • Loading branch information
tjstum authored Dec 1, 2023
2 parents 7b85770 + 4db88d3 commit 7712c98
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 32 deletions.
4 changes: 4 additions & 0 deletions newsfragments/115.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
A deadlock will no longer occur if :func:`trio_asyncio.open_loop`
is cancelled before its first checkpoint. We also now cancel and wait on
all asyncio tasks even if :func:`~trio_asyncio.open_loop` terminates due
to an exception that was raised within the ``async with`` block.
81 changes: 49 additions & 32 deletions trio_asyncio/_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,40 +456,57 @@ async def async_main(*args):
try:
loop._closed = False
async with trio.open_nursery() as tasks_nursery:
await loop._main_loop_init(tasks_nursery)
await loop_nursery.start(loop._main_loop)
yield loop

# Allow all already-submitted tasks a chance to start
# (and then immediately be cancelled), unless the loop
# stops (due to someone else calling stop()) before
# that.
async with trio.open_nursery() as sync_nursery:
sync_nursery.cancel_scope.shield = True

@sync_nursery.start_soon
async def wait_for_sync():
if not loop.is_closed():
await loop.synchronize()
# There are not actually any unshielded checkpoints in
# either of the following async functions, so the
# shield doesn't do much. However, it is necessary to
# make sure that start() actually moves the _main_loop
# task into the tasks_nursery if this call to
# open_loop() is cancelled. TaskStatus.started()
# doesn't complete Nursery.start() if there's a
# cancellation pending, because it figures the task
# will be cancelled soon enough and doesn't want to
# worry about Cancelled exceptions propagating to the
# wrong place; but _main_loop shields everything it does
# after started(), so this just results in start() never
# completing. With the shield here, started() can't see
# the outer cancellation, which avoids the deadlock.
with trio.CancelScope(shield=True):
await loop._main_loop_init(tasks_nursery)
await loop_nursery.start(loop._main_loop)

try:
yield loop
finally:
# Allow all already-submitted tasks a chance to start
# (and then immediately be cancelled), unless the loop
# stops (due to someone else calling stop()) before
# that.
async with trio.open_nursery() as sync_nursery:
sync_nursery.cancel_scope.shield = True

@sync_nursery.start_soon
async def wait_for_sync():
if not loop.is_closed():
await loop.synchronize()
sync_nursery.cancel_scope.cancel()

await loop.wait_stopped()
sync_nursery.cancel_scope.cancel()

await loop.wait_stopped()
sync_nursery.cancel_scope.cancel()

# Cancel and wait on all currently-running tasks.
# Exiting the tasks_nursery will wait for the Trio tasks
# automatically; we mix in the asyncio tasks by scheduling
# a call to run_aio_future() for each one. It's important
# not to wait on one kind of task before the other, so that
# we support Trio tasks that need to run some asyncio
# code during teardown as well as the opposite.
# Like asyncio.run(), we don't bother cancelling and waiting
# on any additional asyncio tasks that these tasks start as they
# unwind.
aio_tasks = asyncio.all_tasks(loop)
for task in aio_tasks:
tasks_nursery.start_soon(run_aio_future, task)
tasks_nursery.cancel_scope.cancel()
# Cancel and wait on all currently-running tasks.
# Exiting the tasks_nursery will wait for the Trio tasks
# automatically; we mix in the asyncio tasks by scheduling
# a call to run_aio_future() for each one. It's important
# not to wait on one kind of task before the other, so that
# we support Trio tasks that need to run some asyncio
# code during teardown as well as the opposite.
# Like asyncio.run(), we don't bother cancelling and waiting
# on any additional asyncio tasks that these tasks start
# as they unwind.
aio_tasks = asyncio.all_tasks(loop)
for task in aio_tasks:
tasks_nursery.start_soon(run_aio_future, task)
tasks_nursery.cancel_scope.cancel()
finally:
try:
await loop._main_loop_exit()
Expand Down

0 comments on commit 7712c98

Please sign in to comment.