[FEATURE] Further develop startup and shutdown events
See original GitHub issueWhile the documentationn for FastAPI is in general extremely solid, there’s a weakpoint that I feel hints at some underdevelopped feature within the framework, and that’s startup and shutdown events. They are briefly mentionned (separately) with the startup event in particular being demonstrated like this :
items = {}
@app.on_event("startup")
async def startup_event():
items["foo"] = {"name": "Fighters"}
items["bar"] = {"name": "Tenders"}
@app.get("/items/{item_id}")
async def read_items(item_id: str):
return items[item_id]
…which could very well be written like this:
items = {
"foo": {"name": "Fighters"},
"bar": {"name": "Tenders"}
}
@app.get("/items/{item_id}")
async def read_items(item_id: str):
return items[item_id]
…and therefore makes the feature look useless. The example for shutdown
instead uses logging as an example, which makes it look like this would be the primary purposes for those events, while in reality, it’s not.
Is your feature request related to a problem? Please describe. The problem is that, throughout the entire documentation, things like database connections are created in the global scope, at module import. While this would be fine in a regular Python application, this has a number of problems, especially with objects that have a side-effect outside the code itself, like database connections. To demonstrate this, I’ve made a test structure that creates a lock file when initialized and deletes it when garbage collected.
Using it like this:
from fastapi import FastAPI
from lock import FileLock
app = FastAPI()
lock = FileLock("fastapi")
@app.get("/")
async def root():
return {"message": "Hello World"}
…does not work and the lock is not deleted before shutdown (I was actually expecting it to be closed properly, like SQLAlchemy does with its connections, but clearly there’s a lot of extra magic going on with SQLAlchemy that I don’t even come close to understanding). This is also extremely apparent when using the --reload
option on Uvicorn, bcause the lock is also not released when the modules are reloaded, causing the import to fail and the server to crash. This would be one thing, but I’ve had a similar incident occur some time ago when, while developping in reload mode, I’ve actually managed to take up every connection on my PostgreSQL server because of that problem, since while SQLAlchemy is smart enough to cleanup on exit where my FileLock
cannot, the same does not happen when hot-reloading code.
So that would be one thing; the documentation should probably go into more details about what those startup and shutdown events are for (the Starlette documentation is a little more concrete about this, but no working code is given to illustrate this) and that should also be woven with the chapters about databases and such to make sure people don’t miss it.
Except… That’s not super ergonomic, now, is it?
from fastapi import FastAPI, Depends
from some_db_module import Connection
app = FastAPI()
_db_conn: Connection
@app.on_event("startup")
def take_lock():
global _db_conn
_db_conn = Connection("mydb:///")
@app.on_event("shutdown")
def release_lock():
global _db_conn
_db_conn.close()
def get_db_conn():
return _db_conn
@app.get("/")
async def root(conn: Connection = Depends(get_db_conn)):
pass
This is basically just a context manager split into two halves, linked together by a global variable. A context manager that will be entered and exited when the ASGI lifetime protocol notifies that the application has started and stopped. A context manager whose only job will be to initialize a resource to be either held while the application is running or used as an injectable dependency. Surely there’s a cleaner way to do this.
Describe the solution you’d like
I’ve been meaning to file this bug for a few weeks now, but what finally got me to do it is the release of FastAPI 0.42 (Good job, everyone!), which has context managers as dependencies as one of its main new features. Not only that, but the examples being given are pretty much all database-related, except connections are opened and closed for each call of each route instead of being polled like SQLAlchemy (and I assume encode’s async database module too). Ideally, events should be replaced with something like this, but where the dependencies are pre-initialized instead of being created on the spot. Maybe by having context managers that are started and stopped based on startup
and shutdown
events and yield “factory functions” that could in turn be called during dependency injection to get the object that needs to be passed.
Something along those lines:
from fastapi import FastAPI, Depends
from sqlalchemy import create_engine
app = FastAPI()
@app.lifetime_dependency
def get_db_conn():
conn_pool = create_engine("mydb:///")
# yield a function that closes around db_conn
# and returns it as a dependency when called
yield lambda: conn_pool
conn_pool.close()
@app.get("/")
async def root(conn: Connection = Depends(get_db_conn)):
pass
Additional context Not sure where else to mention it, but I’ve ran into cases where the shutdown event does not get called before exiting, namely when using the VSCode debugger on Windows and stopping or restarting the application via the debugger’s controls (haven’t tried this on Linux yet). This apparently kills the thread without any sort of cleanup being performed and leaves all database connections open (and possibly unable to timeout, since DBAPI appears to suggest that all queries to be executed as part of a transaction, which most drivers do, and mid-transaction timeout is disabled by default on at least PostgreSQL). I don’t think there is any way that could be fixed, though it should probably be mentionned somewhere, either there or in debugging part of the documentation.
Issue Analytics
- State:
- Created 4 years ago
- Reactions:58
- Comments:39 (26 by maintainers)
Top GitHub Comments
@spate141 This is the pattern I have been using to avoid storing any global variables. This code won’t run as is but you can get the gist of what is going on. Key takeaways are
functools.partial
to pass the app instance to the startup/shutdown tasksapp.state
Depends
which means easier testing later 😄The implementation from encode/starlette#799 just landed in the upstream master. No release as of the time I’m writing this, but enough to start designing around it.
Like I’ve said before, I still believe we should deprecate raw startup and shutdown handlers in favor of wrapping the new generator method in our dependency system. Due to how Starlette is implementing it, some care might need to be taken to avoid breaking existing code using events (the
on_startup
andon_shutdown
parameters toStarlette()
cannot be used at the same time as the newlifespan
parameter, and event handlers declared via the deprecated annotations will simply not fire).