Search code examples
pythonasynchronousrace-conditionaiohttppython-socketio

Is it possible to have race condition in python aiohttp socketio?


I am working on code similar to below code. Sometimes the program stops working or I get strange errors regarding socketio session access. Slowly I feel it could be race conditions.

Its more pseudo code. I want to demonstrate, that I access global shared state and the socketio sessions from multiple coroutines.

import asyncio as aio
from aiohttp import web
import socketio


app = web.Application()
sio = socketio.AsyncServer()

app["sockets"] = []

@sio.on("connect")
async def connect(sid):
    app["sockets"].append(sid)

@sio.on("disconnect")
async def disconnect(sid):
    app["sockets"].remove(sid)

@sio.on("set session")
async def set_session(sid, arg):
    await sio.save_session(sid, {"arg": arg})

async def session_route(req):
    data = await req.json()
    for sid in app["sockets"]:
        await sio.save_session(sid, {"arg": data["arg"]})
    return web.Response(status=200)

if __name__ == '__main__':
    web.run_app(app)

Solution

  • There is definitely a problem here:

    for sid in app["sockets"]:  # you are iterating over a list here
        await sio.save_session(...)  # your coroutine will yield here
    

    You are iterating over the list app["sockets"] and in each iteration you use the await keyword. When the await keyword is used, your coroutine is supended and the event loops checks if another coroutine can be executed or resumed.

    Let's say the connect(...) coroutine is run while session_route is waiting.

    app["sockets"].append(sid)  # this changed the structure of the list
    

    connect(...) changed the structure of the list. This can invalidate all iterators that currently exist for that list. The same goes for the disconnect(...) coroutine.

    So either don't modify the list or at least don't reuse the iterator after the list has changed. The latter solution is easier to achieve here:

    for sid in list(app["sockets"]):
        await sio.save_session(...)
    

    Now the for-loop iterates over a copy of the original list. Changing the list now will not "disturb" the copy.

    Note however, that additions and deletions from the list are not recognized by the copy.

    So, in short, the answer to your question is yes, but it has nothing to do with async io. The same issue can easily occur in synchronous code:

    for i in my_list:
        my_list.remove(1)  # don't do this