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:
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:
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...
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()