<div dir="ltr"><div>I am trying to understand some unexpected behavior in asyncio. My goal is to</div><div>use a custom signal handler to cleanly unwind an asyncio program that has many</div><div>different tasks running. Here's a simplified test case:</div><div><br></div><div>     1  import asyncio, logging, random, signal, sys</div><div>     2  </div><div>     3  logging.basicConfig(level=logging.DEBUG)</div><div>     4  logger = logging.getLogger()</div><div>     5  </div><div>     6  async def main():</div><div>     7      try:</div><div>     8          await asyncio.Event().wait()</div><div>     9      except asyncio.CancelledError:</div><div>    10          <a href="http://logger.info">logger.info</a>('cancelled main()')</div><div>    11          # cleanup logic</div><div>    12  </div><div>    13  def handle_sigint(signal, frame):</div><div>    14      global sigint_count</div><div>    15      global main_coro</div><div>    16      sigint_count += 1</div><div>    17      if sigint_count == 1:</div><div>    18          logger.warn('Received interrupt: shutting down...')</div><div>    19          main_coro.throw(asyncio.CancelledError())</div><div>    20          # missing event loop logic?</div><div>    21      else:</div><div>    22          logger.warn('Received 2nd interrupt: exiting!')</div><div>    23          main_coro.throw(SystemExit(1))</div><div>    24  </div><div>    25  sigint_count = 0</div><div>    26  signal.signal(signal.SIGINT, handle_sigint)</div><div>    27  loop = asyncio.get_event_loop()</div><div>    28  main_coro = main()</div><div>    29  try:</div><div>    30      loop.run_until_complete(main_coro)</div><div>    31  except StopIteration:</div><div>    32      <a href="http://logger.info">logger.info</a>('run_until_complete() finished')</div><div><br></div><div>The main() function is a placeholder that represents some long running task,</div><div>e.g. a server that is waiting for new connections. The handle_sigint() function</div><div>is supposed to attempt to cancel main() so that it can gracefully exit, but if</div><div>it receives a second interrupt, then the process exits immediately.</div><div><br></div><div>Here's an example running the program and then typing Ctrl+C.</div><div><br></div><div>    $ python test.py</div><div>    DEBUG:asyncio:Using selector: EpollSelector</div><div>    ^CWARNING:root:Received interrupt: shutting down...</div><div>    INFO:root:cancelled main()</div><div>    INFO:root:run_until_complete() finished</div><div><br></div><div>This works as I expect it to. Of course my cleanup logic (line 10) isn't</div><div>actually doing anything. In a real server, I might want to send goodbye messages</div><div>to connected clients. To mock this behavior, I'll modify line 11:</div><div><br></div><div>    11          await asyncio.sleep(0)</div><div><br></div><div>Surprisingly, now my cleanup code hangs:</div><div><br></div><div>    $ python test.py</div><div>    DEBUG:asyncio:Using selector: EpollSelector</div><div>    ^CWARNING:root:Received interrupt: shutting down...</div><div>    INFO:root:cancelled main()</div><div>    ^CWARNING:root:Received 2nd interrupt: exiting!</div><div><br></div><div>Notice that the program doesn't exit after the first interrupt. It enters the</div><div>exception handler and appears to hang on the await expression on line 11. I have</div><div>to interrupt it a second time, which throws SystemExit instead.</div><div><br></div><div>I puzzled over this for quite some time until I realized that I can force main()</div><div>to resume by changing line 20:</div><div><br></div><div>    20          main_coro.send(None)</div><div><br></div><div>With this change, the interrupt causes the cleanup logic to run to completion</div><div>and the program exits normally. Of course, if I add a second await expression:</div><div><br></div><div>    11          await asyncio.sleep(0); await asyncio.sleep(0)</div><div><br></div><div>Then I also have to step twice:</div><div><br></div><div>    20          main_coro.send(None); main_coro.send(None)</div><div><br></div><div>My mental model of how the event loop works is pretty poor, but I roughly</div><div>understand that the event loop is responsible for driving coroutines. It appears</div><div>here that the event loop has stopped driving my main() coroutine, and so the</div><div>only way to force it to complete is to call send() from my code.</div><div><br></div><div>Can somebody explain *why* the event loop is not driving my coroutine? Is this a</div><div>bug or am I missing something conceptually?</div><div><br></div><div>More broadly, handling KeyboardInterrupt in async code seems very tricky,</div><div>but I also cannot figure out how to make this interrupt approach work. Is one</div><div>of these better than the other? What is the best practice here? Would it be</div><div>terrible to add `while True: main_coro.send(None)` to my signal handler?</div><div><br></div><div>Thanks,</div><div>Mark</div></div>