Skip to content

Commit f62d446

Browse files
committed
Fix run_until_complete making the next run stop early
1 parent 9d3b0b8 commit f62d446

4 files changed

Lines changed: 82 additions & 2 deletions

File tree

‎Lib/asyncio/base_events.py‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -707,8 +707,19 @@ def run_until_complete(self, future):
707707
# is no need to log the "destroy pending task" message
708708
future._log_destroy_pending = False
709709

710-
future.add_done_callback(_run_until_complete_cb)
710+
# gh-158406: A queued callback can outlive this call. Only let
711+
# it stop the loop while this call is running.
712+
running = True
713+
714+
def done_cb(fut):
715+
if running:
716+
_run_until_complete_cb(fut)
717+
elif not fut.cancelled():
718+
# Still mark the exception as retrieved.
719+
fut.exception()
720+
711721
try:
722+
future.add_done_callback(done_cb)
712723
self.run_forever()
713724
except:
714725
if new_task and future.done() and not future.cancelled():
@@ -718,7 +729,8 @@ def run_until_complete(self, future):
718729
future.exception()
719730
raise
720731
finally:
721-
future.remove_done_callback(_run_until_complete_cb)
732+
running = False
733+
future.remove_done_callback(done_cb)
722734
if not future.done():
723735
raise RuntimeError('Event loop stopped before Future completed.')
724736

‎Lib/test/test_asyncio/test_base_events.py‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -918,6 +918,56 @@ def func():
918918
self.loop.run_forever()
919919
self.assertTrue(func.called)
920920

921+
def test_run_until_complete_interrupted_after_done(self):
922+
# gh-158406: an interrupt after the future is done must not stop
923+
# the next run_until_complete() call early.
924+
self.loop._process_events = mock.Mock()
925+
for exc in (KeyboardInterrupt, SystemExit):
926+
with self.subTest(exc=exc):
927+
def interrupt():
928+
raise exc
929+
930+
async def main():
931+
self.loop.call_soon(interrupt)
932+
933+
with self.assertRaises(exc):
934+
self.loop.run_until_complete(main())
935+
result = self.loop.run_until_complete(asyncio.sleep(0, "ret"))
936+
self.assertEqual(result, "ret")
937+
938+
def test_run_until_complete_stopped_after_done(self):
939+
# gh-158406: stop() in the iteration in which the future is done
940+
# must not stop the next run_until_complete() call early.
941+
async def main():
942+
self.loop.stop()
943+
return 1
944+
945+
self.loop._process_events = mock.Mock()
946+
self.assertEqual(self.loop.run_until_complete(main()), 1)
947+
result = self.loop.run_until_complete(asyncio.sleep(0, "ret"))
948+
self.assertEqual(result, "ret")
949+
950+
def test_run_forever_after_interrupted_run_until_complete(self):
951+
# gh-158406: an interrupt after the future is done must not stop
952+
# a later run_forever() call early.
953+
def interrupt():
954+
raise KeyboardInterrupt
955+
956+
async def main():
957+
self.loop.call_soon(interrupt)
958+
959+
self.loop._process_events = mock.Mock()
960+
with self.assertRaises(KeyboardInterrupt):
961+
self.loop.run_until_complete(main())
962+
963+
def func():
964+
self.loop.stop()
965+
func.called = True
966+
func.called = False
967+
self.loop.call_soon(self.loop.call_soon, func)
968+
self.loop.run_forever()
969+
self.assertTrue(func.called)
970+
921971
def test_single_selecter_event_callback_after_stopping(self):
922972
# Python issue #25593: A stopped event loop may cause event callbacks
923973
# to run more than once.

‎Lib/test/test_asyncio/test_runners.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,20 @@ async def coro():
461461
with self.assertRaises(asyncio.CancelledError):
462462
runner.run(coro())
463463

464+
def test_interrupt_after_main_task_done(self):
465+
assert threading.current_thread() is threading.main_thread()
466+
467+
async def coro():
468+
nonlocal task
469+
task = asyncio.create_task(asyncio.Event().wait())
470+
asyncio.get_running_loop().call_soon(interrupt_self)
471+
472+
task = None
473+
with asyncio.Runner(loop_factory=self.new_loop) as runner:
474+
with self.assertRaises(KeyboardInterrupt):
475+
runner.run(coro())
476+
self.assertTrue(task.cancelled())
477+
464478
def test_signal_install_not_supported_ok(self):
465479
# signal.signal() can throw if the "main thread" doesn't have signals enabled
466480
assert threading.current_thread() is threading.main_thread()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix :meth:`asyncio.loop.run_until_complete` raising :exc:`RuntimeError` when
2+
the previous call was interrupted or stopped just as its future completed.
3+
This could make :func:`asyncio.run` raise :exc:`RuntimeError` instead of
4+
:exc:`KeyboardInterrupt` on Ctrl-C and skip part of its cleanup.

0 commit comments

Comments
 (0)