asyncio - how can I stop (and restart) server without stopping event loop?

Viewed 3366

In this case I'm working with the websockets module. A typical server implementation is this:

import websockets
start_server = websockets.serve(counter, "localhost", 6789)

asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()  

But, I want to be able to stop and restart the server without stopping the event loop. It's not clear to me from the minimal documentation that I've seen about asyncio servers, how to do this. Nor do I know if websockets is implemented in quite the same way.

For instance, if I do something like this:

def counter():
    start_server = websockets.serve(connection_handler, 'localhost', 6789)
    this = loop.run_until_complete(start_server)
    try:
        loop.run_forever()
    finally:
        this.close()
        loop.run_until_complete(this.wait_closed())  


loop = asyncio.get_event_loop()
loop.create_task(anothertask)
startcounter = counter()

I can trigger the server to stop by calling loop.stop(). How do I stop the server without stopping the loop (and upsetting another task running on the loop)?

2 Answers

You can use asyncio.create_task to submit a task when the loop is already running. The run_until_complete() followed by run_forever() pattern is now deprecated because it is incompatible with asyncio.run, which is now the preferred way to run asyncio code.

The recommended approach is to use asyncio.run at top-level to start an async entry point (typically defined as async def main()), and do the rest from there. run_until_complete(x) then becomes simply await x, and run_forever() is not needed because you can await things like server.serve_forever() or an asyncio.Event of your choosing.

Since serve_forever doesn't appear to exist for websockets server, here is the variant with the event (untested):

async def connection_handler(...):
    ...

async def test(stop_request):
    # occasionally stop the server to test it
    while True:
        await asyncio.sleep(1)
        print('requesting stop')
        stop_request.set()

async def main():
    stop_request = asyncio.Event()
    asyncio.create_task(test(stop_request))
    while True:
        print('starting the server')
        server = await websockets.serve(connection_handler, 'localhost', 6789)
        await stop_request.wait()
        stop_request.clear()
        print('stopping the server')
        server.close()
        await server.wait_closed()

asyncio.run(main())

This answer is based on the suggestions and answer provided by user4815162342 as well as borrowing a couple things from the websockets module documentation. The reason for my answer is largely to provide a more fleshed out example. I think that one of the hardest parts of working with asyncio is integrating it with other pieces of code without breaking it.

One thing I want to point out about this implementation is that the "server" object in the line of user4815162342's code:

async def somefunction(): # requires async function
    server = await websockets.serve(handler, 'localhost', 6789)

is the same "server" object that would be created with this bit of code that you will see in the websockets documentation fairly often:

def somefunction(): # non-async function
    start_server = websockets.serve(handler, 'localhost', 6789)
    server = asyncio.run_until_complete(start_server)

And therefore it will respond to the method close().

Furthermore, the author of websockets suggests that the server can be started with:

async def somefunction(): # requires async function
    async with websockets.serve(handler, 'localhost', 6789)
    await something_that_says_stop

Note which of these can be called from a non-async function and which must occur "on the loop".

In my example (a somewhat fanciful bit of script) I integrate a tkinter GUI. This is accomplished with the aiotkinter module which, aside from it's pypi page, has hardly any other mention, let alone example code, on the interwebs. So, here's an example of how to use it with the websockets module in a scenario where the websockets server[s] can be started and stopped.

Notice that I do not call root.mainloop(). Instead, the "blocking line" in this is the asyncio.run() line. Also notice where the set_event_loop_policy line is placed.

This has been tested on mac with python 3.8.2 and TclTk 8.6. Thank you user4815162342 for your help. The language of asyncio is getting clearer and I think we need to get more example code and answers out there that uses the newer language/structures...

import sys
from functools import partial
import asyncio
import aiotkinter
from tkinter import *
from tkinter import messagebox
import json
import websockets
from netifaces import interfaces, ifaddresses, AF_INET


async def register(w, label2):
    USERS.remove(w) if w in USERS else USERS.add(w)
    label2.config(text=f"User Count = {len(USERS)}")


async def connection_handler(websocket, path, stop_request,
                               ticker, start_request, label, label2):
    misterDict = {"Mr.White":"white", "Mr.Orange":"orange",
                "Mr.Blonde":"khaki", "Mr.Pink":"pink",
                "Mr.Blue":"blue", "Mr.Brown":"brown"}

    await register(websocket, label2)
    try:
        message = json.dumps("welcome")
        await websocket.send(message)
        # ignore this -> while server.get():
        async for message in websocket:
            msg = json.loads(message)
            if msg == "end":
                return
            elif msg == "stoptick":
                ticker.set(False)
            elif msg == "starttick":
                ticker.set(True)
                startTicker(ticker, label)
            elif msg == "stopserver":
                stop_request.set()
                break
            elif msg in misterDict.keys():
                color = misterDict[msg]
                changeColor(label, color)
            # ignore this -> await asyncio.sleep(.05)
    except Exception as E1:
        print(f"Error! {E1}")
    finally:
        await register(websocket, label2)

async def main():
    stop_request = asyncio.Event()
    start_request = asyncio.Event()
    # Some tkinter setup
    root = Tk()
    root.title("Color Server")
    root.minsize(250, 250)
    ticker = BooleanVar()
    ticker.set(0)
    server = BooleanVar()
    server.set(0)
    label = Label(master=root, height=4, width=20, bg="white",
                    bd=5)
    label.pack()
    buttonS = Checkbutton(master=root, text='Mr tick', variable=ticker,
                    height=3, command=partial(startTicker,
                    ticker, label))
    buttonS.pack()
    buttonW = Checkbutton(master=root, text='Websocket Server',
                    variable=server, height=3,
                    command=partial(startWeb, server,
                    label, stop_request, start_request))
    buttonW.pack()

    def on_closing():
        if messagebox.askokcancel("Quit", "Do you want to quit?"):
            QUITTER.add("yes")
            if server.get():
                stop_request.set()
            else:
                start_request.set()


    buttonX = Button(master=root, text='Shut down everything', height=2,
                    command=on_closing)
    buttonX.pack()
    label2 = Label(master=root, text='User Count = 0',
                        height=2, bg="white", bd=2)
    label2.pack()
    root.protocol("WM_DELETE_WINDOW", on_closing)

    # websocket server setup code.
    serverlist = set()
    iplist = [ifaddresses(face)[AF_INET][0]["addr"]
        for face in interfaces() if AF_INET in ifaddresses(face)]
    print(f"the interfaces found = {iplist}")
    while True:
        await start_request.wait()
        start_request.clear()
        if "yes" in QUITTER:
            break
        server.set(1)
        for ipadd in iplist: # for each IP address in the system
                                # setup a websocket server
            bound_handler = partial(connection_handler,
                                stop_request=stop_request, ticker=ticker,
                                start_request=start_request,
                                label=label, label2=label2)
            sockserver = await websockets.serve(bound_handler, ipadd, 6789)
            serverlist.add(sockserver)

        await stop_request.wait()
        stop_request.clear()
        for sockserver in serverlist:
            sockserver.close()
            await sockserver.wait_closed()
        serverlist.clear()
        if "yes" in QUITTER:
            break
        server.set(0)


def startWeb(server, label, stop_request, start_request):
    if not server.get():
        stop_request.set()
        return
    else:
        start_request.set()


def changeColor(label, color):
    label.config(bg=color)


async def mRtick(ticker, label):
    ticktock = [("tick", "w"), ("tock", "e")]
    tocker = 0
    while ticker.get():
        a,b = ticktock[tocker]
        label.config(text=a, anchor=b)
        tocker = not tocker
        await asyncio.sleep(0.5)
    label.config(text="")


def startTicker(ticker, label):
    if not ticker.get():
        return
    asyncio.create_task(mRtick(ticker, label))


QUITTER = set()
USERS = set()

asyncio.set_event_loop_policy(aiotkinter.TkinterEventLoopPolicy())
asyncio.run(main())
print("we've ended...")
sys.exit()

Please note that I discovered that I had an unnecessary while loop in the code. This has now been commented out.

Related