python-engineio icon indicating copy to clipboard operation
python-engineio copied to clipboard

Python hang until all clients disconnected

Open ladyisatis opened this issue 1 year ago • 2 comments

Using FastAPI and python-socketio/python-engineio in conjunction with Hypercorn in Asyncio mode, if you CTRL+C or send SIGINT/SIGTERM (no matter on Windows or Linux) the Python script will never exit and socketio.shutdown() will not do anything until all clients are disconnected from the client-side, and then shutdown scripts will proceed. There's no way to detect this from lifespan functions for example,

To reproduce, main.py:

import asyncio
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from hypercorn.config import Config
from hypercorn.asyncio import serve
from socketio import AsyncServer, ASGIApp

@asynccontextmanager
async def lifespan(app: FastAPI):
    print("on_startup")
    yield
    print("on_shutdown")

fastapi_app = FastAPI(lifespan=lifespan)
socketio = AsyncServer(async_mode="asgi")
templates = Jinja2Templates(directory="./html")

fastapi_app.mount("/assets", StaticFiles(directory="./html/assets"), name="assets")

@fastapi_app.get("/", response_class=HTMLResponse)
async def index(request: Request) -> HTMLResponse:
    return templates.TemplateResponse(
        request=request,
        name="index.html",
        context={}
    )

async def main():
    config = Config()
    config.bind = ["127.0.0.1:3005"]

    await serve(
        app=ASGIApp(
            socketio,
            fastapi_app
        ),
        config=config,
        mode='asgi'
    )

if __name__ == '__main__':
    asyncio.run(main())

html/index.html:

<script type="text/javascript" src="/assets/socket.io.min.js"></script>
<div id="status">Not Connected</div>
<script>
    const socket = io("ws://127.0.0.1:3005/", { transports: ["websocket"] });
    socket.on('connect', () => {
        document.getElementById('status').innerText = 'Connected';
    });
    socket.on('disconnect', () => {
        document.getElementById('status').innerText = 'Not Connected';
    });
</script>

And then put socket.io.min.js inside the html/assets folder. Then:

  1. python3 main.py
  2. Go to http://127.0.0.1:3005/ until you see Connected
  3. CTRL+C or send SIGINT to python3 main.py
  4. The server does not shut down
  5. Close the Browser tab of the :3005 tab that's open
  6. After it's closed, the server will then shut down

ladyisatis avatar Jun 08 '24 02:06 ladyisatis

I think this must be an issue with Hypercorn. When running with Uvicorn the Socket.IO connection is properly canceled. This is how I'm testing it:

if __name__ == '__main__':
    uvicorn.run(ASGIApp(socketio, fastapi_app), host='127.0.0.1', port=3005)

miguelgrinberg avatar Jun 09 '24 14:06 miguelgrinberg

Interesting! I'll try and fiddle around with converting the program I had back to Uvicorn, as something odd was going on with Pyinstaller and Windows support so I'd switched to the other alternative I could find. (I dunno if you want me to close this issue and raise the lock issue with the Hypercorn folks instead, but up to you!)

ladyisatis avatar Jun 10 '24 03:06 ladyisatis