miguelgrinberg / python-engineio

Python Engine.IO server and client
MIT License
232 stars 147 forks source link

Python hang until all clients disconnected #357

Open ladyisatis opened 3 months ago

ladyisatis commented 3 months ago

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
miguelgrinberg commented 3 months ago

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)
ladyisatis commented 3 months ago

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!)