I thought this would be a simple thing, but not :-)
I think its even worth a feature request on FastAPI for a "pre-shutdown" event, because it could be simple if embedded in the code there.
So, when running, uvicorn
registers a callback with the event loop to execute when requested to exit. That changes a state in the uvicorn Server object when called once (it sets the server.should_exit
attribute to True). So, if you have a clean way to get the server instance running, you could just poll that attribute in your long-view to see whether it should exit. I found no way to get the a reference to the running server.
So I settled to register another signal handler: one you can have in your app in order to change states as needed. The problem with that: asyncio can only have one handler per signal, when one registers a handler, the previous one is lost. That means installing a custom handler would remove uvicorn's handlers, and it simply would not shut down at all.
To workaround that, I had to introspect the loop._signal_handlers
in the running asyncio loop: this is supposed to be private, but by doing so, I could chain the original signal handler call after a custom signal-handler.
Long story short, this code works to exit the server on the first "ctrl + C":
from fastapi import FastAPI, Request
import asyncio
from uvicorn.server import HANDLED_SIGNALS
from functools import partial
app = FastAPI()
running = True
#@app.on_event("shutdown")
#def shutdown_event():
#global running
#running = False
@app.get("/")
async def index(request: Request):
while running:
await asyncio.sleep(0.1)
@app.on_event("startup")
def chain_signals():
loop = asyncio.get_running_loop()
loop = asyncio.get_running_loop()
signal_handlers = getattr(loop, "_signal_handlers", {}) # disclaimer 1: this is a private attribute: might change without notice.
# Also: unix only, won't work on windows
for sig in HANDLED_SIGNALS:
loop.add_signal_handler(sig, partial(handle_exit, signal_handlers.get(sig, None)) , sig, None)
def handle_exit(original_handler, sig, frame):
global running
running = False
if original_handler:
return original_handler._run() # disclaimer 2: this should be opaque and performed only by the running loop.
# not so bad: this is not changing, and is safe to do.
I'd like to emphasize I was only able to get to this working snippet because you did provide a minimal working example of your problem. You'd get surprised how many question authors do not do so.