question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

[FEATURE] Further develop startup and shutdown events

See original GitHub issue

While 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:open
  • Created 4 years ago
  • Reactions:58
  • Comments:39 (26 by maintainers)

github_iconTop GitHub Comments

9reactions
michaeloliverxcommented, May 13, 2021

@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

  • using functools.partial to pass the app instance to the startup/shutdown tasks
  • storing any arbitrary state on the app.state
  • using the fastapi dependency injection system using Depends which means easier testing later 😄
import dataclasses
from functools import partial

from pydantic import BaseSettings

from fastapi import Depends
from fastapi.applications import FastAPI
from fastapi.requests import Request
from fastapi.routing import APIRouter


class Config(BaseSettings):
    id: str
    password: str


@dataclasses.dataclass
class ClassifyUtil:
    id: str
    password: str

    async def expensive_initialization(self):
        pass

    async def classify_item(self):
        pass


async def startup(app: FastAPI):
    config: Config = app.state.config
    classify_util = ClassifyUtil(config.id, config.password)
    await classify_util.expensive_initialization()
    app.state.classify_util = classify_util


async def shutdown(app: FastAPI):
    pass


router = APIRouter()


async def get_classify_util(request: Request) -> ClassifyUtil:
    if not hasattr(request.app.state, "classify_util"):
        raise AttributeError("classify_util attribute not set on app state")

    classify_util: ClassifyUtil = request.app.state.classify_util
    return classify_util


@router.get("/classify/{my_item}")
async def classify_route(classify_util: ClassifyUtil = Depends(get_classify_util)):
    return await classify_util.classify_item(my_item)


def create_app(config: Config) -> FastAPI:
    config = config or Config()
    app = FastAPI()
    app.state.config = config
    app.add_event_handler(event_type="startup", func=partial(startup, app=app))
    app.add_event_handler(event_type="shutdown", func=partial(shutdown, app=app))
    return app

6reactions
sm-Fifteencommented, May 4, 2020

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 and on_shutdown parameters to Starlette() cannot be used at the same time as the new lifespan parameter, and event handlers declared via the deprecated annotations will simply not fire).

Read more comments on GitHub >

github_iconTop Results From Across the Web

Events: startup - shutdown - FastAPI
In this case, the startup event handler function will initialize the items "database" (just a dict ) with some values. You can add...
Read more >
Engine Startup and Shutdown - TIBCO Product Documentation
Note: During startup, the TIBCO BusinessEvents engine tries to load all the business rules present in the shared folder. Any failure when loading...
Read more >
Startup and Shutdown — Quart 0.17.0 documentation
Startup and Shutdown # ... The ASGI lifespan specification includes the ability for awaiting coroutines before the first byte is received and after...
Read more >
Programming Application Lifecycle Events - Oracle Help Center
Application-scoped startup and shutdown classes have been deprecated as of release 9.0 of WebLogic Server. The information in this chapter about startup and...
Read more >
Startup and Shutdown - NI - National Instruments
ExitApplication event to notify you to exit the application. If the application cancels the shutdown process, the Application Manager control ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found