Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add support for lifespan async context managers (superseding startup and shutdown events) #2944

Merged
merged 31 commits into from
Mar 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
bb2ec92
Add lifespan context argument
uSpike Mar 13, 2021
4620732
Add tests for lifespan context and refactor router event tests
uSpike Mar 13, 2021
8c33c94
Add lifespan docs
uSpike Mar 13, 2021
ecd6d27
Fix lifespan typing
uSpike Mar 13, 2021
c736405
Fix lifespan doc highlight
uSpike Mar 13, 2021
bc1f5fa
Fix event tests repeated code
uSpike Mar 13, 2021
12e924d
Fix import sorting
uSpike Mar 13, 2021
c78ba44
Merge branch 'master' of github.com:tiangolo/fastapi into add-lifespa…
uSpike Dec 19, 2021
c3a3d25
update lifespan to be an async context manager
meshantz Nov 5, 2021
8b1dfd5
Fix formatting
uSpike Dec 19, 2021
48fdb76
Fix type of lifespan to AsyncContextManager[Any]
uSpike Dec 19, 2021
92897b3
Fix docs for lifespan asynccontextmanager
uSpike Dec 19, 2021
6f9ebea
Add lifespan context argument
uSpike Mar 13, 2021
edef8c0
Add tests for lifespan context and refactor router event tests
uSpike Mar 13, 2021
76968ca
Add lifespan docs
uSpike Mar 13, 2021
0dbcb35
Fix lifespan typing
uSpike Mar 13, 2021
2712fb6
Fix lifespan doc highlight
uSpike Mar 13, 2021
d15df2d
Fix event tests repeated code
uSpike Mar 13, 2021
9665645
Fix import sorting
uSpike Mar 13, 2021
628aa71
update lifespan to be an async context manager
meshantz Nov 5, 2021
f612d74
Fix formatting
uSpike Dec 19, 2021
f13274c
Fix type of lifespan to AsyncContextManager[Any]
uSpike Dec 19, 2021
0eed654
Fix docs for lifespan asynccontextmanager
uSpike Dec 19, 2021
81591ba
Use @asynccontextmanager for lifespan for test
JonathanPlasse Oct 16, 2022
d60f9b8
Use FastAPI instead of Starlette for lifespan
JonathanPlasse Oct 21, 2022
2c05463
Replace lifespan AsyncIterator by AsyncGenerator
JonathanPlasse Oct 21, 2022
5ae6547
🔀 Merge master
tiangolo Mar 6, 2023
0ef7cfa
Merge branch 'master' into add-lifespan-context-v3
tiangolo Mar 6, 2023
8b6dd1a
🔀 Merge PR #5503 from @JonathanPlasse with updated types
tiangolo Mar 6, 2023
c842bdd
📝 Update docs for Lifespan events
tiangolo Mar 7, 2023
6adb554
✅ Add tests for new lifespan event docs
tiangolo Mar 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
125 changes: 119 additions & 6 deletions docs/en/docs/advanced/events.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,108 @@
# Events: startup - shutdown
# Lifespan Events

You can define event handlers (functions) that need to be executed before the application starts up, or when the application is shutting down.
You can define logic (code) that should be executed before the application **starts up**. This means that this code will be executed **once**, **before** the application **starts receiving requests**.

These functions can be declared with `async def` or normal `def`.
The same way, you can define logic (code) that should be executed when the application is **shutting down**. In this case, this code will be executed **once**, **after** having handled possibly **many requests**.

Because this code is executed before the application **starts** taking requests, and right after it **finishes** handling requests, it covers the whole application **lifespan** (the word "lifespan" will be important in a second 😉).

This can be very useful for setting up **resources** that you need to use for the whole app, and that are **shared** among requests, and/or that you need to **clean up** afterwards. For example, a database connection pool, or loading a shared machine learning model.

## Use Case

Let's start with an example **use case** and then see how to solve it with this.

Let's imagine that you have some **machine learning models** that you want to use to handle requests. 🤖

The same models are shared among requests, so, it's not one model per request, or one per user or something similar.

Let's imagine that loading the model can **take quite some time**, because it has to read a lot of **data from disk**. So you don't want to do it for every request.

You could load it at the top level of the module/file, but that would also mean that it would **load the model** even if you are just running a simple automated test, then that test would be **slow** because it would have to wait for the model to load before being able to run an independent part of the code.

That's what we'll solve, let's load the model before the requests are handled, but only right before the application starts receiving requests, not while the code is being loaded.

## Lifespan

You can define this *startup* and *shutdown* logic using the `lifespan` parameter of the `FastAPI` app, and a "context manager" (I'll show you what that is in a second).

Let's start with an example and then see it in detail.

We create an async function `lifespan()` with `yield` like this:

```Python hl_lines="16 19"
{!../../../docs_src/events/tutorial003.py!}
```

Here we are simulating the expensive *startup* operation of loading the model by putting the (fake) model function in the dictionary with machine learning models before the `yield`. This code will be executed **before** the application **starts taking requests**, during the *startup*.

And then, right after the `yield`, we unload the model. This code will be executed **after** the application **finishes handling requests**, right before the *shutdown*. This could, for example, release resources like memory or a GPU.

!!! tip
The `shutdown` would happen when you are **stopping** the application.

Maybe you need to start a new version, or you just got tired of running it. 🤷

### Lifespan function

The first thing to notice, is that we are defining an async function with `yield`. This is very similar to Dependencies with `yield`.

```Python hl_lines="14-19"
{!../../../docs_src/events/tutorial003.py!}
```

The first part of the function, before the `yield`, will be executed **before** the application starts.

And the part after the `yield` will be executed **after** the application has finished.

### Async Context Manager

If you check, the function is decorated with an `@asynccontextmanager`.

That converts the function into something called an "**async context manager**".

```Python hl_lines="1 13"
{!../../../docs_src/events/tutorial003.py!}
```

A **context manager** in Python is something that you can use in a `with` statement, for example, `open()` can be used as a context manager:

```Python
with open("file.txt") as file:
file.read()
```

In recent versions of Python, there's also an **async context manager**. You would use it with `async with`:

```Python
async with lifespan(app):
await do_stuff()
```

When you create a context manager or an async context manager like above, what it does is that, before entering the `with` block, it will execute the code before the `yield`, and after exiting the `with` block, it will execute the code after the `yield`.

In our code example above, we don't use it directly, but we pass it to FastAPI for it to use it.

The `lifespan` parameter of the `FastAPI` app takes an **async context manager**, so we can pass our new `lifespan` async context manager to it.

```Python hl_lines="22"
{!../../../docs_src/events/tutorial003.py!}
```

## Alternative Events (deprecated)

!!! warning
Only event handlers for the main application will be executed, not for [Sub Applications - Mounts](./sub-applications.md){.internal-link target=_blank}.
The recommended way to handle the *startup* and *shutdown* is using the `lifespan` parameter of the `FastAPI` app as described above.

## `startup` event
You can probably skip this part.

There's an alternative way to define this logic to be executed during *startup* and during *shutdown*.

You can define event handlers (functions) that need to be executed before the application starts up, or when the application is shutting down.

These functions can be declared with `async def` or normal `def`.

### `startup` event

To add a function that should be run before the application starts, declare it with the event `"startup"`:

Expand All @@ -21,7 +116,7 @@ You can add more than one event handler function.

And your application won't start receiving requests until all the `startup` event handlers have completed.

## `shutdown` event
### `shutdown` event

To add a function that should be run when the application is shutting down, declare it with the event `"shutdown"`:

Expand All @@ -45,3 +140,21 @@ Here, the `shutdown` event handler function will write a text line `"Application

!!! info
You can read more about these event handlers in <a href="https://www.starlette.io/events/" class="external-link" target="_blank">Starlette's Events' docs</a>.

### `startup` and `shutdown` together

There's a high chance that the logic for your *startup* and *shutdown* is connected, you might want to start something and then finish it, acquire a resource and then release it, etc.

Doing that in separated functions that don't share logic or variables together is more difficult as you would need to store values in global variables or similar tricks.

Because of that, it's now recommended to instead use the `lifespan` as explained above.

## Technical Details

Just a technical detail for the curious nerds. 🤓

Underneath, in the ASGI technical specification, this is part of the <a href="https://asgi.readthedocs.io/en/latest/specs/lifespan.html" class="external-link" target="_blank">Lifespan Protocol</a>, and it defines events called `startup` and `shutdown`.

## Sub Applications

🚨 Have in mind that these lifespan events (startup and shutdown) will only be executed for the main application, not for [Sub Applications - Mounts](./sub-applications.md){.internal-link target=_blank}.
28 changes: 28 additions & 0 deletions docs_src/events/tutorial003.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from contextlib import asynccontextmanager

from fastapi import FastAPI


def fake_answer_to_everything_ml_model(x: float):
return x * 42


ml_models = {}


@asynccontextmanager
async def lifespan(app: FastAPI):
# Load the ML model
ml_models["answer_to_everything"] = fake_answer_to_everything_ml_model
yield
# Clean up the ML models and release the resources
ml_models.clear()


app = FastAPI(lifespan=lifespan)


@app.get("/predict")
async def predict(x: float):
result = ml_models["answer_to_everything"](x)
return {"result": result}
3 changes: 3 additions & 0 deletions fastapi/applications.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from enum import Enum
from typing import (
Any,
AsyncContextManager,
Awaitable,
Callable,
Coroutine,
Expand Down Expand Up @@ -71,6 +72,7 @@ def __init__(
] = None,
on_startup: Optional[Sequence[Callable[[], Any]]] = None,
on_shutdown: Optional[Sequence[Callable[[], Any]]] = None,
lifespan: Optional[Callable[["FastAPI"], AsyncContextManager[Any]]] = None,
terms_of_service: Optional[str] = None,
contact: Optional[Dict[str, Union[str, Any]]] = None,
license_info: Optional[Dict[str, Union[str, Any]]] = None,
Expand Down Expand Up @@ -125,6 +127,7 @@ def __init__(
dependency_overrides_provider=self,
on_startup=on_startup,
on_shutdown=on_shutdown,
lifespan=lifespan,
default_response_class=default_response_class,
dependencies=dependencies,
callbacks=callbacks,
Expand Down
3 changes: 3 additions & 0 deletions fastapi/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from enum import Enum, IntEnum
from typing import (
Any,
AsyncContextManager,
Callable,
Coroutine,
Dict,
Expand Down Expand Up @@ -492,6 +493,7 @@ def __init__(
route_class: Type[APIRoute] = APIRoute,
on_startup: Optional[Sequence[Callable[[], Any]]] = None,
on_shutdown: Optional[Sequence[Callable[[], Any]]] = None,
lifespan: Optional[Callable[[Any], AsyncContextManager[Any]]] = None,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
generate_unique_id_function: Callable[[APIRoute], str] = Default(
Expand All @@ -504,6 +506,7 @@ def __init__(
default=default,
on_startup=on_startup,
on_shutdown=on_shutdown,
lifespan=lifespan,
)
if prefix:
assert prefix.startswith("/"), "A path prefix must start with '/'"
Expand Down
97 changes: 59 additions & 38 deletions tests/test_router_events.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
from contextlib import asynccontextmanager
from typing import AsyncGenerator, Dict

import pytest
from fastapi import APIRouter, FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel
Expand All @@ -12,57 +16,49 @@ class State(BaseModel):
sub_router_shutdown: bool = False


state = State()

app = FastAPI()


@app.on_event("startup")
def app_startup():
state.app_startup = True


@app.on_event("shutdown")
def app_shutdown():
state.app_shutdown = True

@pytest.fixture
def state() -> State:
return State()

router = APIRouter()

def test_router_events(state: State) -> None:
app = FastAPI()

@router.on_event("startup")
def router_startup():
state.router_startup = True
@app.get("/")
def main() -> Dict[str, str]:
return {"message": "Hello World"}

@app.on_event("startup")
def app_startup() -> None:
state.app_startup = True

@router.on_event("shutdown")
def router_shutdown():
state.router_shutdown = True
@app.on_event("shutdown")
def app_shutdown() -> None:
state.app_shutdown = True

router = APIRouter()

sub_router = APIRouter()
@router.on_event("startup")
def router_startup() -> None:
state.router_startup = True

@router.on_event("shutdown")
def router_shutdown() -> None:
state.router_shutdown = True

@sub_router.on_event("startup")
def sub_router_startup():
state.sub_router_startup = True
sub_router = APIRouter()

@sub_router.on_event("startup")
def sub_router_startup() -> None:
state.sub_router_startup = True

@sub_router.on_event("shutdown")
def sub_router_shutdown():
state.sub_router_shutdown = True
@sub_router.on_event("shutdown")
def sub_router_shutdown() -> None:
state.sub_router_shutdown = True

router.include_router(sub_router)
app.include_router(router)

@sub_router.get("/")
def main():
return {"message": "Hello World"}


router.include_router(sub_router)
app.include_router(router)


def test_router_events():
assert state.app_startup is False
assert state.router_startup is False
assert state.sub_router_startup is False
Expand All @@ -85,3 +81,28 @@ def test_router_events():
assert state.app_shutdown is True
assert state.router_shutdown is True
assert state.sub_router_shutdown is True


def test_app_lifespan_state(state: State) -> None:
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
state.app_startup = True
yield
state.app_shutdown = True

app = FastAPI(lifespan=lifespan)

@app.get("/")
def main() -> Dict[str, str]:
return {"message": "Hello World"}

assert state.app_startup is False
assert state.app_shutdown is False
with TestClient(app) as client:
assert state.app_startup is True
assert state.app_shutdown is False
response = client.get("/")
assert response.status_code == 200, response.text
assert response.json() == {"message": "Hello World"}
assert state.app_startup is True
assert state.app_shutdown is True