Skip to content

Commit fb4772d

Browse files
authored
Merge pull request python-trio#81 from oremanj/loop-exit
2 parents 04947c3 + 876f236 commit fb4772d

File tree

3 files changed

+42
-24
lines changed

3 files changed

+42
-24
lines changed

newsfragments/80.bugfix.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Previously, cancelling the context surrounding an :func:`open_loop`
2+
block might cause a deadlock in some cases. The ordering of operations
3+
during loop teardown has been improved, so this shouldn't happen
4+
anymore.

trio_asyncio/_base.py

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -687,8 +687,19 @@ async def _main_loop(self, task_status=trio.TASK_STATUS_IGNORED):
687687
sniffio.current_async_library_cvar.set("asyncio")
688688

689689
try:
690-
while not self._stopped.is_set():
691-
await self._main_loop_one()
690+
# The shield here ensures that if the context surrounding
691+
# the loop is cancelled, we keep processing callbacks
692+
# until we reach the callback inserted by stop().
693+
# That's important to maintain the asyncio invariant
694+
# that everything you schedule before stop() will run
695+
# before the loop stops. In order to be safe against
696+
# deadlocks, it's important that the surrounding
697+
# context ensure that stop() gets called upon a
698+
# cancellation. (open_loop() does this indirectly
699+
# by calling _main_loop_exit().)
700+
with trio.CancelScope(shield=True):
701+
while not self._stopped.is_set():
702+
await self._main_loop_one()
692703
except StopAsyncIteration:
693704
# raised by .stop_me() to interrupt the loop
694705
pass
@@ -745,16 +756,27 @@ async def _main_loop_exit(self):
745756
if self._closed:
746757
return
747758

748-
self.stop()
749-
await self.wait_stopped()
750-
751-
while True:
752-
try:
753-
await self._main_loop_one(no_wait=True)
754-
except trio.WouldBlock:
755-
break
756-
except StopAsyncIteration:
757-
pass
759+
with trio.CancelScope(shield=True):
760+
# wait_stopped() will return once _main_loop() exits.
761+
# stop() inserts a callback that will cause such, and
762+
# _main_loop() doesn't block except to wait for new
763+
# callbacks to be added, so this should be deadlock-proof.
764+
self.stop()
765+
await self.wait_stopped()
766+
767+
# Drain all remaining callbacks, even those after an initial
768+
# call to stop(). This avoids deadlocks in some cases if
769+
# work is submitted to the loop after the shutdown process
770+
# starts. TODO: figure out precisely what this helps with,
771+
# maybe find a better way. test_wrong_context_manager_order
772+
# deadlocks if we remove it for now.
773+
while True:
774+
try:
775+
await self._main_loop_one(no_wait=True)
776+
except trio.WouldBlock:
777+
break
778+
except StopAsyncIteration:
779+
pass
758780

759781
# Kill off unprocessed work
760782
self._cancel_fds()

trio_asyncio/_loop.py

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -389,11 +389,6 @@ async def async_main(*args):
389389
"""
390390

391391
# TODO: make sure that there is no asyncio loop already running
392-
393-
def _main_loop_exit(self):
394-
super()._main_loop_exit()
395-
self._thread = None
396-
397392
async with trio.open_nursery() as nursery:
398393
loop = TrioEventLoop(queue_len=queue_len)
399394
old_loop = current_loop.set(loop)
@@ -404,14 +399,11 @@ def _main_loop_exit(self):
404399
await yield_(loop)
405400
finally:
406401
try:
407-
await loop.stop().wait()
402+
await loop._main_loop_exit()
408403
finally:
409-
try:
410-
await loop._main_loop_exit()
411-
finally:
412-
loop.close()
413-
nursery.cancel_scope.cancel()
414-
current_loop.reset(old_loop)
404+
loop.close()
405+
nursery.cancel_scope.cancel()
406+
current_loop.reset(old_loop)
415407

416408

417409
def run(proc, *args, queue_len=None):

0 commit comments

Comments
 (0)