Triggering "after_server_start" event when running asyncio server

Not sure whether I should ask this here or open a Github Issue…

Some sanic plugins/extensions rely on the “before_server_start” and/or “after_server_start” events to trigger initialization routines.

My Sanic-Plugins-Framwork and several of the SPF-built plugins used listeners on “before_server_start” for some start-up tasks.

After release of Sanic 19.6 and the introduction of ASGS mode, it came to my attention that some plugins broke, because ASGS-mode is designed to trigger the “after_server_start” event only. It didn’t seem like a big difference, so I changed SPF and Sanic-CORS to now run their startup routines on “after_server_start” to maintain compatibility going forward.

This has how opened the gates to some other problems. Namely, for those who use app.create_server() with return_asyncio_server=True rather than app.run().

See issues here: https://github.com/ashleysommer/sanicpluginsframework/issues/12
and here: https://github.com/ashleysommer/sanic-cors/issues/35

When you use app.create_server() with return_asyncio_server=True, it triggers the “before_server_start” but it never triggers “after_server_start”.

I believe the intention is that the sanic user is supposed to trigger the “after_server_start” event manually after running asyncio.ensure_future(server) but before running loop.run_forever().

However I can’t see an easy way for a user to do that. The list of listener functions to call on the event is created by the _helper() function, and stored in the server_settings object. But when the server coroutine is handed back to the caller, there is no way of getting that server_settings object.

Ideally we’d want to do something like this:

loop = asyncio.get_event_loop()
server = app.create_server(host=host, port=hostport, return_asyncio_server=True)
server_settings = #somehow get access to the server_settings created in create_server
task = asyncio.ensure_future(server, loop=loop)
trig = app.trigger_events(server_settings.get("after_start", []), loop)
trig_task = asyncio.ensure_future(trig, loop=loop)
loop.run_forever()

Does anyone have any comments or suggestions around how to correctly do this?

2 Likes

a bit hacky but if you only need the after_server_start of the settings then this might work:

after_start = [partial(listener, app) for listener in app.listeners["after_server_start"]]
trig = app.trigger_events(after_start , loop)

I think that perhaps we just need to settle on a recommendation and put it in the docs as a “gotcha” if you plan to roll the server yourself.

Another option might be to simply provide a method that does this in a cleaner fashion:

server = app.create_server(...)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(server)

server.complete_startup()

loop.run_forever()
1 Like

I’ve come up with an implementation that allows the user to do this:

def run():
    loop = asyncio.get_event_loop()
    create_server_coro = app.create_server(
        host='0.0.0.0',
        port=8000,
        return_asyncio_server=True,
        asyncio_server_kwargs=None,
        debug=True
    )
    create_server_coro = asyncio.ensure_future(create_server_coro, loop=loop)
    server = loop.run_until_complete(create_server_coro)
    server.after_start()
    try:
        loop.run_forever()
    except KeyboardInterrupt:
        server.before_stop()
        loop.stop()
    finally:
        server.after_stop()

Here, create_server() returns an unexecuted coroutine like normal. But the difference is when you enque that, not only does it start the asyncio_server, but if you await the result, it gives you a SanicAsyncServer object, which wraps the asyncio_server and provides some convenience functions. This allows you to run after_started() on it.

Bonus features, you can now also use it to run before_stop() and after_stop(), because those too were not previously available to users of create_server().

Best part is, this actually shouldn’t break existing code. It is completely compatible with a method that looks like this:

def run():
    loop = asyncio.get_event_loop()
    server = app.create_server(
        host='0.0.0.0',
        port=8000,
        return_asyncio_server=True,
        asyncio_server_kwargs=None,
        debug=True
    )
    asyncio.ensure_future(server, loop=loop)
    try:
        loop.run_forever()
    except KeyboardInterrupt:
        loop.stop()

I’ll clean up the implementation and make a PR.

PR: https://github.com/huge-success/sanic/pull/1676