miguelgrinberg / flask-sock

Modern WebSocket support for Flask.
MIT License
271 stars 24 forks source link

gunicorn: each request kills a thread when it's done #78

Closed snarfed closed 4 months ago

snarfed commented 4 months ago

Hi! First off, thank you for flask-sock, simple-websocket, and all of your other work on Flask and websockets. It's great!

I'm trying to debug an odd problem. I have flask-sock working ok and serving websocket requests, using gunicorn with threads, but each request seems to close (kill) its thread after it finishes. Specifically, if I run gunicorn --workers 1 --threads 20, and use a client that holds websocket requests open for an hour and then disconnects and reconnects, my app serves 20 of these requests successfully, and then starts serving 504s.

I'm sure this is my own fault, but I'm struggling to figure out how to fix it. Details and code below. Does this sound familiar? Any idea what I'm doing wrong? Thank you in advance!

I'm on Python 3.11, flask-sock 0.7.0, simple-websocket 1.0.0, Flask 3.0.2, Werkzeug 3.0.1, gunicorn 21.2.0, Ubuntu 22. I run gunicorn with gunicorn --workers 1 --threads 20 -b :$PORT hub:app. It's running in Google App Engine Flex, which is a relatively standard autoscaled WSGI host.

Simplified code:

from flask_sock import Sock
from simple_websocket import ConnectionClosed

def subscribe():
    while True:
        for commit in read_commits():  # blocks until there are new commits
            yield commit

def subscription():
    def handler(ws):
        logger.debug(f'New websocket client for {nsid}')
        try:
            for commit in subscribe():
                logger.debug(f'Sending to websocket client: {commit}')
                ws.send(encode(commit))
        except ConnectionClosed as cc:
            logger.debug(f'Websocket client disconnected')

    return handler

sock = Sock(app)
# I register the route dynamically in my code, keeping it that way here in case it matters
sock.route(f'/xrpc/subscribe')(subscription())
snarfed commented 4 months ago

If it helps, I see New websocket client... and Sending to websocket client... messages in my logs, but not Websocket client disconnected. I also don't see any exceptions or stack traces in logs, stdout, or stderr. I guess it's possible that an uncaught exception is happening somewhere, but it seems unlikely.

miguelgrinberg commented 4 months ago

Your threads are likely blocking on the read_commits() call. If the thread is blocked there waiting for something, then the client going away and closing the WebSocket connection is not going to unblock it, because this is waiting on something else that is unrelated. The thread is going to be released only when you attempt to read or write on the closed WebSocket connection.

You can verify this with a simple test. Connect a client and wait for the thread to block on the for-loop. Now disconnect the client, and nothing will happen (i.e. no disconnection in the log). Now do what is necessary for the read_commits() function to return an item. At that point your thread will try to write to the WebSocket and this will raise ConnectionClosed, log the disconnection and release the thread.

snarfed commented 4 months ago

Ah, right! That makes perfect sense. Thank you! I'll confirm and then close this issue.

snarfed commented 4 months ago

Confirmed. Thank you again!