{"id":13482226,"url":"https://github.com/cjrh/aiorun","last_synced_at":"2025-05-13T23:05:02.783Z","repository":{"id":26214154,"uuid":"107533067","full_name":"cjrh/aiorun","owner":"cjrh","description":"A \"run\" function for asyncio-based apps that does all the boilerplate.","archived":false,"fork":false,"pushed_at":"2025-01-27T15:00:39.000Z","size":165,"stargazers_count":458,"open_issues_count":2,"forks_count":26,"subscribers_count":11,"default_branch":"master","last_synced_at":"2025-05-11T01:04:24.436Z","etag":null,"topics":["async","async-await","asynchronous","asynchronous-tasks","asyncio","asyncio-python"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cjrh.png","metadata":{"files":{"readme":"README.rst","changelog":"CHANGES","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":["cjrh"]}},"created_at":"2017-10-19T10:38:34.000Z","updated_at":"2025-05-09T07:19:35.000Z","dependencies_parsed_at":"2024-01-05T21:20:14.286Z","dependency_job_id":"e49884a0-d045-4154-85b4-8601fc53ec9e","html_url":"https://github.com/cjrh/aiorun","commit_stats":{"total_commits":136,"total_committers":12,"mean_commits":"11.333333333333334","dds":"0.11764705882352944","last_synced_commit":"bc4519849d7863f8408da401ca738394cca76c41"},"previous_names":[],"tags_count":34,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cjrh%2Faiorun","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cjrh%2Faiorun/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cjrh%2Faiorun/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cjrh%2Faiorun/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cjrh","download_url":"https://codeload.github.com/cjrh/aiorun/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254040606,"owners_count":22004577,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","async-await","asynchronous","asynchronous-tasks","asyncio","asyncio-python"],"created_at":"2024-07-31T17:01:00.074Z","updated_at":"2025-05-13T23:04:57.771Z","avatar_url":"https://github.com/cjrh.png","language":"Python","funding_links":["https://github.com/sponsors/cjrh"],"categories":["Python","Misc","其他"],"sub_categories":[],"readme":".. image:: https://github.com/cjrh/aiorun/workflows/Python%20application/badge.svg\n    :target: https://github.com/cjrh/aiorun/actions\n\n.. image:: https://coveralls.io/repos/github/cjrh/aiorun/badge.svg?branch=master\n    :target: https://coveralls.io/github/cjrh/aiorun?branch=master\n\n.. image:: https://img.shields.io/pypi/pyversions/aiorun.svg\n    :target: https://pypi.python.org/pypi/aiorun\n\n.. image:: https://img.shields.io/github/tag/cjrh/aiorun.svg\n    :target: https://img.shields.io/github/tag/cjrh/aiorun.svg\n\n.. image:: https://img.shields.io/badge/install-pip%20install%20aiorun-ff69b4.svg\n    :target: https://img.shields.io/badge/install-pip%20install%20aiorun-ff69b4.svg\n\n.. image:: https://img.shields.io/pypi/v/aiorun.svg\n    :target: https://pypi.org/project/aiorun/\n\n.. image:: https://img.shields.io/badge/calver-YYYY.MM.MINOR-22bfda.svg\n    :alt: This project uses calendar-based versioning scheme\n    :target: http://calver.org/\n\n.. image:: https://pepy.tech/badge/aiorun\n    :alt: Downloads\n    :target: https://pepy.tech/project/aiorun\n\n.. image:: https://img.shields.io/badge/code%20style-black-000000.svg\n    :alt: This project uses the \"black\" style formatter for Python code\n    :target: https://github.com/python/black\n\n\n.. contents:: Table of Contents\n\n🏃 aiorun\n======================\n\nHere's the big idea (how you use it):\n\n.. code-block:: python\n\n   import asyncio\n   from aiorun import run\n\n   async def main():\n       # Put your application code here\n       await asyncio.sleep(1.0)\n\n   if __name__ == '__main__':\n       run(main())\n\nThis package provides a ``run()`` function as the starting point\nof your ``asyncio``-based application. The ``run()`` function will\nrun forever. If you want to shut down when ``main()`` completes, just\ncall ``loop.stop()`` inside it: that will initiate shutdown.\n\n.. warning::\n\n    Note that `aiorun.run(coro)` will run **forever**, unlike the standard\n    library's ``asyncio.run()`` helper. You can call `aiorun.run()`\n    without a coroutine parameter, and it will still run forever.\n\n    This is surprising to many people, because they sometimes expect that\n    unhandled exceptions should abort the program, with an exception and\n    a traceback. If you want this behaviour, please see the section on\n    *error handling* further down.\n\n.. warning::\n\n    Note that `aiorun.run(coro)` will create a **new event loop instance**\n    every time it is invoked (same as `asyncio.run`). This might cause\n    confusing errors if your code interacts with the default event loop\n    instance provided by the stdlib `asyncio` library. For such situations\n    you can provide the actual loop you're using with\n    `aiorun.run(coro, loop=loop)`. There is more info about this further down.\n\n    However, generally speaking, configuring your own loop and providing\n    it in this way is a code smell. You will find it much easier to\n    reason about your code if you do all your task creation *inside*\n    an async context, such as within an `async def` function, because then\n    there will no ambiguity about which event loop is in play: it will\n    always be the one returned by `asyncio.get_running_loop()`.\n\n\n🤔 Why?\n----------------\n\nThe ``run()`` function will handle **everything** that normally needs\nto be done during the shutdown sequence of the application.  All you\nneed to do is write your coroutines and run them.\n\nSo what the heck does ``run()`` do exactly?? It does these standard,\nidiomatic actions for asyncio apps:\n\n- creates a ``Task`` for the given coroutine (schedules it on the\n  event loop),\n- calls ``loop.run_forever()``,\n- adds default (and smart) signal handlers for both ``SIGINT``\n  and ``SIGTERM`` that will stop the loop;\n- and *when* the loop stops (either by signal or called directly), then it will...\n- ...gather all outstanding tasks,\n- cancel them using ``task.cancel()``,\n- resume running the loop until all those tasks are done,\n- wait for the *executor* to complete shutdown, and\n- finally close the loop.\n\nAll of this stuff is boilerplate that you will never have to write\nagain. So, if you use ``aiorun`` this is what **you** need to remember:\n\n- Spawn all your work from a single, starting coroutine\n- When a shutdown signal is received, **all** currently-pending tasks\n  will have ``CancelledError`` raised internally. It's up to you whether\n  you want to handle this inside each coroutine with\n  a ``try/except`` or not.\n- If you want to protect coros from cancellation, see `shutdown_waits_for()`\n  further down.\n- Try to have executor jobs be shortish, since the shutdown process will wait\n  for them to finish. If you need a long-running thread or process tasks, use\n  a dedicated thread/subprocess and set ``daemon=True`` instead.\n\nThere's not much else to know for general use. `aiorun` has a few special\ntools that you might need in unusual circumstances. These are discussed\nnext.\n\n🖥️ What about TCP server startup?\n-----------------------------------\n\nYou will see in many examples online that for servers, startup happens in\nseveral ``run_until_complete()`` phases before the primary ``run_forever()``\nwhich is the \"main\" running part of the program. How do we handle that with\n*aiorun*?\n\nLet's recreate the `echo client \u0026 server \u003chttps://docs.python.org/3/library/asyncio-stream.html#tcp-echo-client-using-streams\u003e`_\nexamples from the Standard Library documentation:\n\n**Client:**\n\n.. code-block:: python\n\n    # echo_client.py\n    import asyncio\n    from aiorun import run\n\n    async def tcp_echo_client(message):\n        # Same as original!\n        reader, writer = await asyncio.open_connection('127.0.0.1', 8888)\n        print('Send: %r' % message)\n        writer.write(message.encode())\n        data = await reader.read(100)\n        print('Received: %r' % data.decode())\n        print('Close the socket')\n        writer.close()\n        asyncio.get_event_loop().stop()  # Exit after one msg like original\n\n    message = 'Hello World!'\n    run(tcp_echo_client(message))\n\n**Server:**\n\n.. code-block:: python\n\n    import asyncio\n    from aiorun import run\n\n    async def handle_echo(reader, writer):\n        # Same as original!\n        data = await reader.read(100)\n        message = data.decode()\n        addr = writer.get_extra_info('peername')\n        print(\"Received %r from %r\" % (message, addr))\n        print(\"Send: %r\" % message)\n        writer.write(data)\n        await writer.drain()\n        print(\"Close the client socket\")\n        writer.close()\n\n    async def main():\n        server = await asyncio.start_server(handle_echo, '127.0.0.1', 8888)\n        print('Serving on {}'.format(server.sockets[0].getsockname()))\n        async with server:\n            await server.serve_forever()\n\n    run(main())\n\nIt works the same as the original examples, except you see this\nwhen you hit ``CTRL-C`` on the server instance:\n\n.. code-block:: bash\n\n    $ python echo_server.py\n    Running forever.\n    Serving on ('127.0.0.1', 8888)\n    Received 'Hello World!' from ('127.0.0.1', 57198)\n    Send: 'Hello World!'\n    Close the client socket\n    ^CStopping the loop\n    Entering shutdown phase.\n    Cancelling pending tasks.\n    Cancelling task:  \u003cTask pending coro=[...snip...]\u003e\n    Running pending tasks till complete\n    Waiting for executor shutdown.\n    Leaving. Bye!\n\nTask gathering, cancellation, and executor shutdown all happen\nautomatically.\n\n🐛 Error Handling\n------------------\n\nUnlike the standard library's ``asyncio.run()`` method, ``aiorun.run``\nwill run forever, and does not stop on unhandled exceptions. This is partly\nbecause we predate the standard library method, during the time in which\n``run_forever()`` was actually the recommended API for servers, and partly\nbecause it can *make sense* for long-lived servers to be resilient to\nunhandled exceptions.  For example, if 99% of your API works fine, but the\none new endpoint you just added has a bug: do you really want that one new\nendpoint to crash-loop your deployed service?\n\nNevertheless, not all usages of ``aiorun`` are long-lived servers, so some\nusers would prefer that ``aiorun.run()`` crash on an unhandled exception,\njust like any normal Python program.  For this, we have an extra parameter\nthat enables it:\n\n.. code-block:: python\n\n   # stop_demo.py\n   from aiorun import run\n\n   async def main():\n       raise Exception('ouch')\n\n   if __name__ == '__main__':\n       run(main(), stop_on_unhandled_errors=True)\n\nThis produces the following output:\n\n.. code-block::\n\n    $ python stop_demo.py\n    Unhandled exception; stopping loop.\n    Traceback (most recent call last):\n      File \"/opt/project/examples/stop_unhandled.py\", line 9, in \u003cmodule\u003e\n        run(main(), stop_on_unhandled_errors=True)\n      File \"/opt/project/aiorun.py\", line 294, in run\n        raise pending_exception_to_raise\n      File \"/opt/project/aiorun.py\", line 206, in new_coro\n        await coro\n      File \"/opt/project/examples/stop_unhandled.py\", line 5, in main\n        raise Exception(\"ouch\")\n    Exception: ouch\n\nError handling scenarios can get very complex, and I suggest that you\ntry to keep your error handling as simple as possible. Nevertheless, sometimes\npeople have special needs that require some complexity, so let's look at a\nfew scenarios where error-handling considerations can be more challenging.\n\n``aiorun.run()`` can also be started without an initial coroutine, in which\ncase any other created tasks still run as normal; in this case exceptions\nstill abort the program if the parameter is supplied:\n\n.. code-block:: python\n\n    import asyncio\n    from aiorun import run\n\n\n    async def job():\n        raise Exception(\"ouch\")\n\n\n    if __name__ == \"__main__\":\n        loop = asyncio.new_event_loop()\n        asyncio.set_event_loop(loop)\n        loop.create_task(job())\n\n        run(loop=loop, stop_on_unhandled_errors=True)\n\nThe output is the same as the previous program. In this second example,\nwe made a our own loop instance and passed that to ``run()``. It is also possible\nto configure your exception handler on the loop, but if you do this the\n``stop_on_unhandled_errors`` parameter is no longer allowed:\n\n.. code-block:: python\n\n    import asyncio\n    from aiorun import run\n\n\n    async def job():\n        raise Exception(\"ouch\")\n\n\n    if __name__ == \"__main__\":\n        loop = asyncio.new_event_loop()\n        asyncio.set_event_loop(loop)\n        loop.create_task(job())\n        loop.set_exception_handler(lambda loop, context: \"Error\")\n\n        run(loop=loop, stop_on_unhandled_errors=True)\n\nBut this is not allowed:\n\n.. code-block::\n\n    Traceback (most recent call last):\n      File \"/opt/project/examples/stop_unhandled_illegal.py\", line 15, in \u003cmodule\u003e\n        run(loop=loop, stop_on_unhandled_errors=True)\n      File \"/opt/project/aiorun.py\", line 171, in run\n        raise Exception(\n    Exception: If you provide a loop instance, and you've configured a\n    custom exception handler on it, then the 'stop_on_unhandled_errors'\n    parameter is unavailable (all exceptions will be handled).\n    /usr/local/lib/python3.8/asyncio/base_events.py:633:\n        RuntimeWarning: coroutine 'job' was never awaited\n\nRemember that the parameter ``stop_on_unhandled_errors`` is just a convenience. If you're\ngoing to go to the trouble of making your own loop instance anyway, you can\nstop the loop yourself inside your own exception handler just fine, and\nthen you no longer need to set ``stop_on_unhandled_errors``:\n\n.. code-block:: python\n\n    # custom_stop.py\n    import asyncio\n    from aiorun import run\n\n\n    async def job():\n        raise Exception(\"ouch\")\n\n\n    async def other_job():\n        try:\n            await asyncio.sleep(10)\n        except asyncio.CancelledError:\n            print(\"other_job was cancelled!\")\n\n\n    if __name__ == \"__main__\":\n        loop = asyncio.new_event_loop()\n        asyncio.set_event_loop(loop)\n        loop.create_task(job())\n        loop.create_task(other_job())\n\n        def handler(loop, context):\n            # https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.call_exception_handler\n            print(f'Stopping loop due to error: {context[\"exception\"]} ')\n            loop.stop()\n\n        loop.set_exception_handler(handler=handler)\n\n        run(loop=loop)\n\nIn this example, we schedule two jobs on the loop. One of them raises an\nexception, and you can see in the output that the other job was still\ncancelled during shutdown as expected (which is what you expect ``aiorun``\nto do!):\n\n.. code-block::\n\n    $ python custom_stop.py\n    Stopping loop due to error: ouch\n    other_job was cancelled!\n\nNote however that in this situation the exception is being *handled* by\nyour custom exception handler, and does not bubble up out of the ``run()``\nlike you saw in earlier examples. If you want to do something with that\nexception, like reraise it or something, you need to capture it inside your\ncustom exception handler and then do something with it, like add it to a list\nthat you check after ``run()`` completes, and then reraise there or something\nsimilar.\n\n💨 Do you like `uvloop \u003chttps://github.com/magicstack/uvloop\u003e`_?\n------------------------------------------------------------------\n\n.. code-block:: python\n\n   import asyncio\n   from aiorun import run\n\n   async def main():\n       \u003csnip\u003e\n\n   if __name__ == '__main__':\n       run(main(), use_uvloop=True)\n\nNote that you have to ``pip install uvloop`` yourself.\n\n🛡️ Smart shield for shutdown\n---------------------------------\n\nIt's unusual, but sometimes you're going to want a coroutine to not get\ninterrupted by cancellation *during the shutdown sequence*. You'll look in\nthe official docs and find ``asyncio.shield()``.\n\nUnfortunately, ``shield()`` doesn't work in shutdown scenarios because\nthe protection offered by ``shield()`` only applies if the specific coroutine\n*inside which* the ``shield()`` is used, gets cancelled directly.\n\nLet me explain: if you do a conventional shutdown sequence (like ``aiorun``\nis doing internally), this is the sequence of steps:\n\n- ``tasks = all_tasks()``, followed by\n- ``[t.cancel() for t in tasks]``, and then\n- ``run_until_complete(gather(*tasks))``\n\nThe way ``shield()`` works internally is it creates a *secret, inner*\ntask—which also gets included in the ``all_tasks()`` call above! Thus\nit also receives a cancellation exception just like everything else.\n\nTherefore, we have an alternative version of ``shield()`` that works better for\nus: ``shutdown_waits_for()``. If you've got a coroutine that must **not** be\ncancelled during the shutdown sequence, just wrap it in\n``shutdown_waits_for()``!\n\nHere's an example:\n\n.. code-block:: python\n\n    import asyncio\n    from aiorun import run, shutdown_waits_for\n\n    async def corofn():\n        for i in range(10):\n            print(i)\n            await asyncio.sleep(1)\n        print('done!')\n\n    async def main():\n        try:\n            await shutdown_waits_for(corofn())\n        except asyncio.CancelledError:\n            print('oh noes!')\n\n    run(main())\n\nIf you hit ``CTRL-C`` *before* 10 seconds has passed, you will see\n``oh noes!`` printed immediately, and then after 10 seconds (since start),\n``done!`` is printed, and thereafter the program exits.\n\nOutput:\n\n.. code-block:: shell\n\n    $ python testshield.py\n    0\n    1\n    2\n    3\n    4\n    ^CStopping the loop\n    oh noes!\n    5\n    6\n    7\n    8\n    9\n    done!\n\nBehind the scenes, ``all_tasks()`` would have been cancelled by ``CTRL-C``,\n*except* ones wrapped in ``shutdown_waits_for()`` calls.  In this respect, it\nis loosely similar to ``asyncio.shield()``, but with special applicability\nto our shutdown scenario in ``aiorun()``.\n\nBe careful with this: the coroutine should still finish up at some point.\nThe main use case for this is short-lived tasks that you don't want to\nwrite explicit cancellation handling.\n\nOh, and you can use ``shutdown_waits_for()`` as if it were ``asyncio.shield()``\ntoo. For that use-case it works the same.  If you're using ``aiorun``, there\nis no reason to use ``shield()``.\n\n🙏 Windows Support\n-------------------------\n\n``aiorun`` also supports Windows! Kinda. Sorta. The root problem with Windows,\nfor a thing like ``aiorun`` is that Windows doesn't support *signal handling*\nthe way Linux or Mac OS X does. Like, at all.\n\nFor Linux, ``aiorun`` does \"the right thing\" out of the box for the\n``SIGINT`` and ``SIGTERM`` signals; i.e., it will catch them and initiate\na safe shutdown process as described earlier. However, on *Windows*, these\nsignals don't work.\n\nThere are two signals that work on Windows: the ``CTRL-C`` signal (happens\nwhen you press, unsurprisingly, ``CTRL-C``, and the ``CTRL-BREAK`` signal\nwhich happens when you...well, you get the picture.\n\nThe good news is that, for ``aiorun``, both of these will work. Yay! The bad\nnews is that for them to work, you have to run your code in a Console\nwindow. Boo!\n\nFortunately, it turns out that you can run an asyncio-based process *not*\nattached to a Console window, e.g. as a service or a subprocess, *and* have\nit also receive a signal to safely shut down in a controlled way. It turns\nout that it is possible to send a ``CTRL-BREAK`` signal to another process,\nwith no console window involved, but only as long as that process was created\nin a particular way and---here is the drop---this targetted process is a\nchild process of the one sending the signal. Yeah, I know, it's a downer.\n\nThere is an example of how to do this in the tests:\n\n.. code-block:: python3\n\n    import subprocess as sp\n\n    proc = sp.Popen(\n        ['python', 'app.py'],\n        stdout=sp.PIPE,\n        stderr=sp.STDOUT,\n        creationflags=sp.CREATE_NEW_PROCESS_GROUP\n    )\n    print(proc.pid)\n\nNotice how we print out the process id (``pid``). Then you can send that\nprocess the signal from a completely different process, once you know\nthe ``pid``:\n\n.. code-block:: python3\n\n    import os, signal\n\n    os.kill(pid, signal.CTRL_BREAK_EVENT)\n\n(Remember, ``os.kill()`` doesn't actually kill, it only sends a signal)\n\n``aiorun`` supports this use-case above, although I'll be pretty surprised\nif anyone actually uses it to manage microservices (does anyone do this?)\n\nSo to summarize: ``aiorun`` will do a controlled shutdown if either\n``CTRL-C`` or ``CTRL-BREAK`` is entered via keyboard in a Console window\nwith a running instance, or if the ``CTRL-BREAK`` signal is sent to\na *subprocess* that was created with the ``CREATE_NEW_PROCESS_GROUP``\nflag set. `Here \u003chttps://stackoverflow.com/a/35792192\u003e`_ is a much more\ndetailed explanation of these issues.\n\nFinally, ``uvloop`` is not yet supported on Windows so that won't work\neither.\n\nAt the very least, ``aiorun`` will, well, *run* on Windows ¯\\\\_(ツ)_/¯\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcjrh%2Faiorun","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcjrh%2Faiorun","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcjrh%2Faiorun/lists"}