[Python-Dev] Asynchronous context manager in a typical network server

Szieberth Ádám sziebadam at gmail.com
Fri Dec 18 08:58:55 EST 2015


Hi Developers!

This is my first post. Please excuse me my poor English. If anyone is
interested, I wrote a small introduction on my homepage. Link is at the bottom.

This post is about how to effectively implement the new asynchronous context
manager in a typical network server.

I would appreciate and welcome any confirmation or critics whether my thinking
is right or wrong. Thanks in advance!

So, a typical server main code I used to see around is like this:

    srv = loop.run_until_complete(create_server(handler, host, port))
    try:
        loop.run_forever()
    except KeyboardInterrupt:
        pass
    finally:
        # other tear down code may be here
        srv.close()
        loop.run_until_complete(srv.wait_closed())
    loop.close()

Note that `create_server()` here is not necessary
`BaseEventLoop.create_server()`.

The above code is not prepared to handle `OSError`s or any other `Exception`s
(including a `KeyboardInterrupt` by a rapid Ctr+C) when setting up the server,
it just prints the traceback to the console which is not user friendly.
Moreover, I would expect from a server to handle the SIGTERM signal as well
and tell its clients that it stops serving when not force killed.

How the main code should create server, maintain the serving, deal with errors
and close properly both the connections and the event loop when exiting
without letting pending tasks around is not trivial. There are many questions
on SO and other places of the internet regarding of this problem.

My idea was to provide a simple code which is robust in terms of these
concerns by profiting from the new asynchronous context manager pattern.

The code of the magic methods of a typical awaitable `CreateServer` object
seems rather trivial:

    async def __aenter__(self):
        self.server = await self
        return self.server

    async def __aexit__(self, exc_type, exc_value, traceback):
        # other tear down code may be here
        self.server.close()
        await self.server.wait_closed()

However, to make it work, a task has to be created:

    async def server_task():
        async with CreateServer(handler, host, port) as srv:
            await asyncio.Future()  # wait forever

I write some remarks regarding the above code to the end of this post. Note
that `srv` is unreachable from outside which could be a problem in some cases.
What is unavoidable: this task has to get cancelled explicitely by the main
code which should look like this:

    srvtsk = loop.create_task(server_task())

    signal.signal(signal.SIGTERM, lambda si, fr: loop.call_soon(srvtsk.cancel))

    while True:
        try:
            loop.run_until_complete(srvtsk)
        except KeyboardInterrupt:
            srvtsk.cancel()
        except asyncio.CancelledError:
            break
        except Exception as err:
            print(err)
            break
    loop.close()

Note that when `CancelledError` gets raised, the tear down process is already
done.

Remarks:

* It would be nice to have an `asyncio.wait_forever()` coroutine for dummy
  context bodies.
* Moreover, I also imagined an `BaseEventLoop.create_context_task(awithable,
  body_coro_func=None)` method. The `body_coro_func` should default to
  `asyncio.wait_forever()`, otherwise it should get whatever is returned by
  `__aenter__` as a single argument. The returned Task object should also
  provide a reference to that object.

Best regards,
Ádám

(http://szieberthadam.github.io/)


More information about the Python-Dev mailing list