Package: aiorun
Table of Contents🏃 aiorun🤔 Why?🖥️ What about TCP server startup?🐛 Error Handling💨 Do you like uvloop?🛡️ Smart shield for shutdown🙏 Windows Support🏃 aiorunHere’s the big idea (how you use it):importasynciofromaiorunimportrunasyncdefmain():# Put your application code hereawaitasyncio.sleep(1.0)if__name__=='__main__':run(main())This package provides arun()function as the starting point
of yourasyncio-based application. Therun()function will
run forever. If you want to shut down whenmain()completes, just
callloop.stop()inside it: that will initiate shutdown.WarningNote thataiorun.run(coro)will runforever, unlike the standard
library’sasyncio.run()helper. You can callaiorun.run()without a coroutine parameter, and it will still run forever.This is surprising to many people, because they sometimes expect that
unhandled exceptions should abort the program, with an exception and
a traceback. If you want this behaviour, please see the section onerror handlingfurther down.WarningNote thataiorun.run(coro)will create anew event loop instanceevery time it is invoked (same asasyncio.run). This might cause
confusing errors if your code interacts with the default event loop
instance provided by the stdlibasynciolibrary. For such situations
you can provide the actual loop you’re using withaiorun.run(coro, loop=loop). There is more info about this further down.However, generally speaking, configuring your own loop and providing
it in this way is a code smell. You will find it much easier to
reason about your code if you do all your task creationinsidean async context, such as within anasync deffunction, because then
there will no ambiguity about which event loop is in play: it will
always be the one returned byasyncio.get_running_loop().🤔 Why?Therun()function will handleeverythingthat normally needs
to be done during the shutdown sequence of the application.  All you
need to do is write your coroutines and run them.So what the heck doesrun()do exactly?? It does these standard,
idiomatic actions for asyncio apps:creates aTaskfor the given coroutine (schedules it on the
event loop),callsloop.run_forever(),adds default (and smart) signal handlers for bothSIGINTandSIGTERMthat will stop the loop;andwhenthe loop stops (either by signal or called directly), then it will……gather all outstanding tasks,cancel them usingtask.cancel(),resume running the loop until all those tasks are done,wait for theexecutorto complete shutdown, andfinally close the loop.All of this stuff is boilerplate that you will never have to write
again. So, if you useaiorunthis is whatyouneed to remember:Spawn all your work from a single, starting coroutineWhen a shutdown signal is received,allcurrently-pending tasks
will haveCancelledErrorraised internally. It’s up to you whether
you want to handle this inside each coroutine with
atry/exceptor not.If you want to protect coros from cancellation, seeshutdown_waits_for()further down.Try to have executor jobs be shortish, since the shutdown process will wait
for them to finish. If you need a long-running thread or process tasks, use
a dedicated thread/subprocess and setdaemon=Trueinstead.There’s not much else to know for general use.aiorunhas a few special
tools that you might need in unusual circumstances. These are discussed
next.🖥️ What about TCP server startup?You will see in many examples online that for servers, startup happens in
severalrun_until_complete()phases before the primaryrun_forever()which is the “main” running part of the program. How do we handle that withaiorun?Let’s recreate theecho client & serverexamples from the Standard Library documentation:Client:# echo_client.pyimportasynciofromaiorunimportrunasyncdeftcp_echo_client(message):# Same as original!reader,writer=awaitasyncio.open_connection('127.0.0.1',8888)print('Send:%r'%message)writer.write(message.encode())data=awaitreader.read(100)print('Received:%r'%data.decode())print('Close the socket')writer.close()asyncio.get_event_loop().stop()# Exit after one msg like originalmessage='Hello World!'run(tcp_echo_client(message))Server:importasynciofromaiorunimportrunasyncdefhandle_echo(reader,writer):# Same as original!data=awaitreader.read(100)message=data.decode()addr=writer.get_extra_info('peername')print("Received%rfrom%r"%(message,addr))print("Send:%r"%message)writer.write(data)awaitwriter.drain()print("Close the client socket")writer.close()asyncdefmain():server=awaitasyncio.start_server(handle_echo,'127.0.0.1',8888)print('Serving on{}'.format(server.sockets[0].getsockname()))asyncwithserver:awaitserver.serve_forever()run(main())It works the same as the original examples, except you see this
when you hitCTRL-Con the server instance:$pythonecho_server.pyRunningforever.Servingon('127.0.0.1',8888)Received'Hello World!'from('127.0.0.1',57198)Send:'Hello World!'Closetheclientsocket^CStoppingtheloopEnteringshutdownphase.Cancellingpendingtasks.Cancellingtask:<Taskpendingcoro=[...snip...]>RunningpendingtaskstillcompleteWaitingforexecutorshutdown.Leaving.Bye!Task gathering, cancellation, and executor shutdown all happen
automatically.🐛 Error HandlingUnlike the standard library’sasyncio.run()method,aiorun.runwill run forever, and does not stop on unhandled exceptions. This is partly
because we predate the standard library method, during the time in whichrun_forever()was actually the recommended API for servers, and partly
because it canmake sensefor long-lived servers to be resilient to
unhandled exceptions.  For example, if 99% of your API works fine, but the
one new endpoint you just added has a bug: do you really want that one new
endpoint to crash-loop your deployed service?Nevertheless, not all usages ofaiorunare long-lived servers, so some
users would prefer thataiorun.run()crash on an unhandled exception,
just like any normal Python program.  For this, we have an extra parameter
that enables it:# stop_demo.pyfromaiorunimportrunasyncdefmain():raiseException('ouch')if__name__=='__main__':run(main(),stop_on_unhandled_errors=True)This produces the following output:$ python stop_demo.py
Unhandled exception; stopping loop.
Traceback (most recent call last):
  File "/opt/project/examples/stop_unhandled.py", line 9, in <module>
    run(main(), stop_on_unhandled_errors=True)
  File "/opt/project/aiorun.py", line 294, in run
    raise pending_exception_to_raise
  File "/opt/project/aiorun.py", line 206, in new_coro
    await coro
  File "/opt/project/examples/stop_unhandled.py", line 5, in main
    raise Exception("ouch")
Exception: ouchError handling scenarios can get very complex, and I suggest that you
try to keep your error handling as simple as possible. Nevertheless, sometimes
people have special needs that require some complexity, so let’s look at a
few scenarios where error-handling considerations can be more challenging.aiorun.run()can also be started without an initial coroutine, in which
case any other created tasks still run as normal; in this case exceptions
still abort the program if the parameter is supplied:importasynciofromaiorunimportrunasyncdefjob():raiseException("ouch")if__name__=="__main__":loop=asyncio.new_event_loop()asyncio.set_event_loop(loop)loop.create_task(job())run(loop=loop,stop_on_unhandled_errors=True)The output is the same as the previous program. In this second example,
we made a our own loop instance and passed that torun(). It is also possible
to configure your exception handler on the loop, but if you do this thestop_on_unhandled_errorsparameter is no longer allowed:importasynciofromaiorunimportrunasyncdefjob():raiseException("ouch")if__name__=="__main__":loop=asyncio.new_event_loop()asyncio.set_event_loop(loop)loop.create_task(job())loop.set_exception_handler(lambdaloop,context:"Error")run(loop=loop,stop_on_unhandled_errors=True)But this is not allowed:Traceback (most recent call last):
  File "/opt/project/examples/stop_unhandled_illegal.py", line 15, in <module>
    run(loop=loop, stop_on_unhandled_errors=True)
  File "/opt/project/aiorun.py", line 171, in run
    raise Exception(
Exception: If you provide a loop instance, and you've configured a
custom exception handler on it, then the 'stop_on_unhandled_errors'
parameter is unavailable (all exceptions will be handled).
/usr/local/lib/python3.8/asyncio/base_events.py:633:
    RuntimeWarning: coroutine 'job' was never awaitedRemember that the parameterstop_on_unhandled_errorsis just a convenience. If you’re
going to go to the trouble of making your own loop instance anyway, you can
stop the loop yourself inside your own exception handler just fine, and
then you no longer need to setstop_on_unhandled_errors:# custom_stop.pyimportasynciofromaiorunimportrunasyncdefjob():raiseException("ouch")asyncdefother_job():try:awaitasyncio.sleep(10)exceptasyncio.CancelledError:print("other_job was cancelled!")if__name__=="__main__":loop=asyncio.new_event_loop()asyncio.set_event_loop(loop)loop.create_task(job())loop.create_task(other_job())defhandler(loop,context):# https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.call_exception_handlerprint(f'Stopping loop due to error:{context["exception"]}')loop.stop()loop.set_exception_handler(handler=handler)run(loop=loop)In this example, we schedule two jobs on the loop. One of them raises an
exception, and you can see in the output that the other job was still
cancelled during shutdown as expected (which is what you expectaiorunto do!):$ python custom_stop.py
Stopping loop due to error: ouch
other_job was cancelled!Note however that in this situation the exception is beinghandledby
your custom exception handler, and does not bubble up out of therun()like you saw in earlier examples. If you want to do something with that
exception, like reraise it or something, you need to capture it inside your
custom exception handler and then do something with it, like add it to a list
that you check afterrun()completes, and then reraise there or something
similar.💨 Do you likeuvloop?importasynciofromaiorunimportrunasyncdefmain():<snip>if__name__=='__main__':run(main(),use_uvloop=True)Note that you have topip install uvloopyourself.🛡️ Smart shield for shutdownIt’s unusual, but sometimes you’re going to want a coroutine to not get
interrupted by cancellationduring the shutdown sequence. You’ll look in
the official docs and findasyncio.shield().Unfortunately,shield()doesn’t work in shutdown scenarios because
the protection offered byshield()only applies if the specific coroutineinside whichtheshield()is used, gets cancelled directly.Let me explain: if you do a conventional shutdown sequence (likeaiorunis doing internally), this is the sequence of steps:tasks = all_tasks(), followed by[t.cancel() for t in tasks], and thenrun_until_complete(gather(*tasks))The wayshield()works internally is it creates asecret, innertask—which also gets included in theall_tasks()call above! Thus
it also receives a cancellation exception just like everything else.Therefore, we have an alternative version ofshield()that works better for
us:shutdown_waits_for(). If you’ve got a coroutine that mustnotbe
cancelled during the shutdown sequence, just wrap it inshutdown_waits_for()!Here’s an example:importasynciofromaiorunimportrun,shutdown_waits_forasyncdefcorofn():foriinrange(10):print(i)awaitasyncio.sleep(1)print('done!')asyncdefmain():try:awaitshutdown_waits_for(corofn())exceptasyncio.CancelledError:print('oh noes!')run(main())If you hitCTRL-Cbefore10 seconds has passed, you will seeoh noes!printed immediately, and then after 10 seconds (since start),done!is printed, and thereafter the program exits.Output:$pythontestshield.py01234^CStoppingtheloopohnoes!56789done!Behind the scenes,all_tasks()would have been cancelled byCTRL-C,exceptones wrapped inshutdown_waits_for()calls.  In this respect, it
is loosely similar toasyncio.shield(), but with special applicability
to our shutdown scenario inaiorun().Be careful with this: the coroutine should still finish up at some point.
The main use case for this is short-lived tasks that you don’t want to
write explicit cancellation handling.Oh, and you can useshutdown_waits_for()as if it wereasyncio.shield()too. For that use-case it works the same.  If you’re usingaiorun, there
is no reason to useshield().🙏 Windows Supportaiorunalso supports Windows! Kinda. Sorta. The root problem with Windows,
for a thing likeaiorunis that Windows doesn’t supportsignal handlingthe way Linux or Mac OS X does. Like, at all.For Linux,aiorundoes “the right thing” out of the box for theSIGINTandSIGTERMsignals; i.e., it will catch them and initiate
a safe shutdown process as described earlier. However, onWindows, these
signals don’t work.There are two signals that work on Windows: theCTRL-Csignal (happens
when you press, unsurprisingly,CTRL-C, and theCTRL-BREAKsignal
which happens when you…well, you get the picture.The good news is that, foraiorun, both of these will work. Yay! The bad
news is that for them to work, you have to run your code in a Console
window. Boo!Fortunately, it turns out that you can run an asyncio-based processnotattached to a Console window, e.g. as a service or a subprocess,andhave
it also receive a signal to safely shut down in a controlled way. It turns
out that it is possible to send aCTRL-BREAKsignal to another process,
with no console window involved, but only as long as that process was created
in a particular way and—here is the drop—this targetted process is a
child process of the one sending the signal. Yeah, I know, it’s a downer.There is an example of how to do this in the tests:importsubprocessasspproc=sp.Popen(['python','app.py'],stdout=sp.PIPE,stderr=sp.STDOUT,creationflags=sp.CREATE_NEW_PROCESS_GROUP)print(proc.pid)Notice how we print out the process id (pid). Then you can send that
process the signal from a completely different process, once you know
thepid:importos,signalos.kill(pid,signal.CTRL_BREAK_EVENT)(Remember,os.kill()doesn’t actually kill, it only sends a signal)aiorunsupports this use-case above, although I’ll be pretty surprised
if anyone actually uses it to manage microservices (does anyone do this?)So to summarize:aiorunwill do a controlled shutdown if eitherCTRL-CorCTRL-BREAKis entered via keyboard in a Console window
with a running instance, or if theCTRL-BREAKsignal is sent to
asubprocessthat was created with theCREATE_NEW_PROCESS_GROUPflag set.Hereis a much more
detailed explanation of these issues.Finally,uvloopis not yet supported on Windows so that won’t work
either.At the very least,aiorunwill, well,runon Windows ¯\_(ツ)_/¯