Search code examples
pythonmultithreadingflaskwerkzeug

Werkzeug BaseWSGIServer Thread Hangs after Handling Request. Terminal Unresponsive


This has stopped me from doing anything useful for more than a week - if you have experience with Python/Werkzeug/Flask/Multi-threading, please help...

Now, normally, you would run a Flask app in a separate terminal, via py flask.py, needing to manually spin it up and manually turn it off with Ctrl-C. My Python App only needs this local server in certain cases, for example: to get a token using Spotify's Authorization Code Flow so that I can later make valid requests using their Web API.

My auth_server.py file is the "section" responsible for all of this. Here lives a Class named LocalServerThread, that subclasses Thread, taken word-for-word from this answer. 8 years ago, this was the only useful answer for Windows in a sea of shit; alas, unfortunately, today it is only a partially-working example.

Here it is in it's full glory:

from werkzeug.serving import make_server

class ServerThread(threading.Thread):
    THREAD_NAME = 'LocalServerThread'

    def __init__(self, app):
        threading.Thread.__init__(self, name=LocalServerThread.THREAD_NAME)
        self.server = make_server('127.0.0.1', 8080, app)
        self.ctx = app.app_context()
        self.ctx.push()

    def run(self):
        log.info('starting server')
        self.server.serve_forever()

    def shutdown(self):
        self.server.shutdown()

def start_server():
    global server
    app = flask.Flask('myapp')
    # App routes defined here
    server = ServerThread(app)
    server.start()
    log.info('server started')

def stop_server():
    global server
    server.shutdown()

Here is the rest of the code in question in my auth_server.py:

def start_local_http_server():
    app = Flask(__name__)

    @app.route('/')
    def home():

    @app.route('/login')
    def login():

    @app.route('/callback')
    def callback():

    global server
    server = LocalServerThread(app)
    server.start()
    print(fr'Started {LocalServerThread.THREAD_NAME}, serving: http://127.0.0.1:8080')

def stop_local_http_server():
    server.shutdown()

Here is my actual code from main.py that is leveraging this code:

if not auth_server.validate_token(reject=True):
    auth_server.start_local_http_server()

    time.sleep(4)

    auth_server.stop_local_http_server()
    
    print('Finally Done')

Now here is where it all Falls Apart...
I run it with py ./main.py. If I just let it run, shutdown will work perfectly, shutting everything down correctly. Good.
However, if, while the main thread sleeps in those 4 seconds, I go to any of the routes ('/', '/login', '/callback'), and the server successfully serves me the page, then when the program reaches self.server.shutdown(), it will just hang around there anywhere from 90-300 seconds before finally exiting and reaching print('Finally Done') back in the main thread. During this time, the terminal will look like this, refusing to let the program exit: The Terminal while it Hangs

This is flippin' infuriating. I can't figure out what the heck is going in during debug as well. self.server.shutdown() goes deeper into socketserver.py BaseServer shutdown() method and I can't step into anything farther than the two lines within, like it's waiting around for something... But I got you the call stack:

These are the Call Stacks side by side, right before the debug loses track of what's going on.
First: Standard Run. ________________________ Second: going to '/' during the 4sec sleep:

Call Stack without Get Call Stack with Get

Note: One more time, I am on Windows.
Is there anyone out there looking at this that has the knowledge/expertise to see the issue? This would really help not only me, but our posterity as well, I suspect...


Solution

  • I tried your code (I didn't see a class LocalServerThread, which you mention) and when I tested it by invoking the / endpoint it does hang on the call to stop_local_http_server -- that is, until I re-invoke the / endpoint and then everything terminates.

    Since that is not a satisfactory solution for your situation, I suggest you try running the server in a multiprocessing.Process instance, which can be terminated by the main process. In the following code I have "encapsulated" the Flask app and endpoint functions within the process's worker function:

    import multiprocessing
    import logging
    
    from werkzeug.serving import make_server
    from flask import Flask
    
    log = logging.getLogger(__name__)
    
    def worker():
        app = Flask(__name__)
    
        @app.route('/')
        def home():
            return 'It works!'
    
        @app.route('/login')
        def login():
            ...
    
        @app.route('/callback')
        def callback():
            ...
    
        server = make_server('127.0.0.1', 8080, app)
        ctx = app.app_context()
        ctx.push()
    
        log.info('starting server')
        server.serve_forever()
    
    def start_local_http_server():
        global server_process
    
        server_process = multiprocessing.Process(target=worker, name="ServerProcess")
        server_process.start()
        print('Started ServerProcess, serving: http://127.0.0.1:8080')
    
    def stop_local_http_server():
        server_process.terminate()
    
    if __name__ == '__main__':
        import time
    
        start_local_http_server()
    
        time.sleep(10)  # I need more time to invoke an endpoint
    
        stop_local_http_server()
    

    Update

    Per the suggestion offered by AKX, I modified the your code (more or less) to use wsgiref.simple_server.make_server with multithreading and it does seem to work. The only potential issue is that the shutdown method is not documented, although it clearly currently exists due to class WSGIServer being a sublclass of http.server.HTTPServer, which in turn is ultimately a subclass of socketserver.BaseServer that does have a shutdown method.

    import threading
    import logging
    from wsgiref.simple_server import make_server
    
    from flask import Flask
    
    log = logging.getLogger(__name__)
    
    class ServerThread(threading.Thread):
        THREAD_NAME = 'ServerThread'
    
        def __init__(self, app):
            threading.Thread.__init__(self, name=ServerThread.THREAD_NAME)
            self.server = make_server('127.0.0.1', 8080, app)
            ctx = app.app_context()
            ctx.push()
    
        def run(self):
            log.info('starting server')
            self.server.serve_forever()
    
        def shutdown(self):
            self.server.shutdown()
    
    def start_local_http_server():
        global server_thread
    
        app = Flask(__name__)
    
        # App routes defined here
    
        @app.route('/')
        def home():
            return 'It works!'
    
        @app.route('/login')
        def login():
            ...
    
        @app.route('/callback')
        def callback():
            ...
    
        server_thread = ServerThread(app)
        server_thread.start()
        log.info('server started')
    
    def stop_local_http_server():
        server_thread.shutdown()
    
    if __name__ == '__main__':
        import time
    
        start_local_http_server()
    
        time.sleep(10)  # I need more time to invoke an endpoint
    
        print('Terminating server.')
        stop_local_http_server()