<div dir="ltr">On Sat, Aug 5, 2017 at 9:41 PM, Chris Jerdonek <span dir="ltr"><<a href="mailto:chris.jerdonek@gmail.com" target="_blank">chris.jerdonek@gmail.com</a>></span> wrote:<br><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">I want to share a pattern I came up with for handling interrupt<br>signals in asyncio to see if you had any feedback (ways to make it<br>easier, similar approaches, etc).<br></blockquote><div> </div>Just after sending this email, I learned that approaches like this can't work in general since they can't interrupt a "tight loop," and that changes to asyncio are needed.<br><br>There are some discussions about this on GitHub:<br><br><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">The signal solution is indeed nicer, because it ensures that interrupts are<br>treated as regular asyncio events, but it means you can't interrupt code<br>that's stuck in a tight CPU loop (e.g. while True: pass), and it requires<br>more sophistication from users.</blockquote><br>(from: <a href="https://github.com/python/asyncio/pull/305#issuecomment-168541045">https://github.com/python/asyncio/pull/305#issuecomment-168541045</a> )<div><br><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">This is a big no-no. In the first version of uvloop I did exactly this -- handle SIGINT and let the loop to handle it asynchronously. It was completely unusable. Turns out people write tight loops quite frequently, and inability to stop your Python program with Ctrl-C is something they aren't prepared to handle at all.</blockquote><br>(from: <a href="https://github.com/python/asyncio/issues/341#issuecomment-236443331">https://github.com/python/asyncio/issues/341#issuecomment-236443331</a> )<br><br>The current open issue is here:<br><a href="https://github.com/python/asyncio/issues/341">https://github.com/python/asyncio/issues/341</a><br><br>--Chris<br></div><div class="gmail_extra"><br><div class="gmail_quote"><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"><br>
I wanted something that was easy to check and reason about. I'm<br>
already familiar with some of the pitfalls in handling signals, for<br>
example as described in Nathaniel's Control-C blog post announced<br>
here:<br>
<a href="https://mail.python.org/pipermail/async-sig/2017-April/thread.html" rel="noreferrer" target="_blank">https://mail.python.org/<wbr>pipermail/async-sig/2017-<wbr>April/thread.html</a><br>
<br>
The basic idea is to create a Future to run alongside the main<br>
coroutine whose only purpose is to "catch" the signal. And then call--<br>
<br>
    asyncio.wait(futures, return_when=asyncio.FIRST_<wbr>COMPLETED)<br>
<br>
When a signal is received, both tasks stop, and then you have access<br>
to the main task (which will be pending) for things like cleanup and<br>
inspection.<br>
<br>
One advantage of this approach is that it lets you put all your<br>
cleanup logic in the main program instead of putting some of it in the<br>
signal handler. You also don't need to worry about things like<br>
handling KeyboardInterrupt at arbitrary points in your code.<br>
<br>
I'm including the code at bottom.<br>
<br>
On the topic of asyncio.run() that I mentioned in an earlier email<br>
[1], it doesn't look like the run() API posted in PR #465 [2] has<br>
hooks to support what I'm describing (but I could be wrong). So maybe<br>
this is another use case that the future API should contemplate.<br>
<br>
--Chris<br>
<br>
[1] <a href="https://mail.python.org/pipermail/async-sig/2017-August/000373.html" rel="noreferrer" target="_blank">https://mail.python.org/<wbr>pipermail/async-sig/2017-<wbr>August/000373.html</a><br>
[2] <a href="https://github.com/python/asyncio/pull/465" rel="noreferrer" target="_blank">https://github.com/python/<wbr>asyncio/pull/465</a><br>
<br>
<br>
import asyncio<br>
import io<br>
import signal<br>
<br>
def _cleanup(loop):<br>
    try:<br>
        loop.run_until_complete(loop.<wbr>shutdown_asyncgens())<br>
    finally:<br>
        loop.close()<br>
<br>
def handle_sigint(future):<br>
    future.set_result(signal.<wbr>SIGINT)<br>
<br>
async def run():<br>
    print('running...')<br>
    await asyncio.sleep(1000000)<br>
<br>
def get_message(sig, task):<br>
    stream = io.StringIO()<br>
    task.print_stack(file=stream)<br>
    traceback = stream.getvalue()<br>
    return f'interrupted by {<a href="http://sig.name" rel="noreferrer" target="_blank">sig.name</a>}:\n{traceback}'<br>
<br>
def main(coro):<br>
    loop = asyncio.new_event_loop()<br>
<br>
    try:<br>
        # This is made truthy if the loop is interrupted by a signal.<br>
        interrupted = []<br>
<br>
        future = asyncio.Future(loop=loop)<br>
        future.add_done_callback(<wbr>lambda future: interrupted.append(1))<br>
<br>
        loop.add_signal_handler(<wbr>signal.SIGINT, handle_sigint, future)<br>
<br>
        futures = [future, coro]<br>
        future = asyncio.wait(futures, return_when=asyncio.FIRST_<wbr>COMPLETED)<br>
        done, pending = loop.run_until_complete(<wbr>future)<br>
<br>
        if interrupted:<br>
            # Do whatever cleanup you want here and/or get the stacktrace<br>
            # of the interrupted main task.<br>
            sig = done.pop().result()<br>
            task = pending.pop()<br>
            msg = get_message(sig, task)<br>
<br>
            task.cancel()<br>
            raise KeyboardInterrupt(msg)<br>
<br>
    finally:<br>
        _cleanup(loop)<br>
<br>
main(run())<br>
<br>
<br>
Below is what the code above outputs if you run it and then press Control-C:<br>
<br>
running...<br>
^CTraceback (most recent call last):<br>
  File "test-signal.py", line 54, in <module><br>
    main(run())<br>
  File "test-signal.py", line 49, in main<br>
    raise KeyboardInterrupt(msg)<br>
KeyboardInterrupt: interrupted by SIGINT:<br>
Stack for <Task pending coro=<run() running at test-signal.py:17><br>
wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at<br>
0x10fe9b9a8>()]>> (most recent call last):<br>
  File "test-signal.py", line 17, in run<br>
    await asyncio.sleep(1000000)<br>
</blockquote></div><br></div></div>