Lifespan: запуск и остановка приложения
lifespan — это async-контекстный менеджер, который выполняет код один раз при старте приложения (до yield) и один раз при остановке (после yield); он пришёл на смену устаревшим @app.on_event.
Некоторые вещи делаются не на каждый запрос, а один раз на жизнь процесса: открыть пул соединений к БД, прогреть кэш, загрузить ML-модель. Это и есть зона ответственности
lifespan.
До запроса нужно подготовить общие для всего приложения ресурсы. Создавать пул соединений к базе на каждый запрос — расточительно; его открывают один раз при старте и закрывают при остановке. Раньше для этого были декораторы @app.on_event("startup") и @app.on_event("shutdown"), но они объявлены устаревшими. Современный и рекомендуемый способ — единый lifespan-контекстменеджер.
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
# --- startup ---
app.state.db_pool = await create_pool() # один раз при старте
print("приложение запущено, пул создан")
yield
# --- shutdown ---
await app.state.db_pool.close() # один раз при остановке
print("приложение остановлено, пул закрыт")
app = FastAPI(lifespan=lifespan)
Логика та же, что у зависимости с yield, только в масштабе всего приложения: код до yield — старт, код после — остановка. Общие ресурсы удобно класть в app.state, откуда их потом достают зависимости. Преимущество единого менеджера в том, что старт и связанная с ним остановка живут рядом, в одном месте, а не разнесены по двум декораторам.
Как работает под капотом
ASGI-сервер при запуске отправляет приложению событие lifespan.startup, прокручивая менеджер до yield, а при остановке — lifespan.shutdown, дотягивая его. Между ними обрабатываются запросы. Смоделируем жизненный цикл на stdlib:
def lifespan():
print("STARTUP: открываем пул соединений к БД")
state = {"pool": "db-pool", "ready": True}
yield state
print("SHUTDOWN: закрываем пул соединений")
state["ready"] = False
gen = lifespan()
state = next(gen) # сервер запускается -> startup
print("сервер работает, ready =", state["ready"])
print("обрабатываем запрос 1, используем", state["pool"])
print("обрабатываем запрос 2, используем", state["pool"])
try:
next(gen) # сервер останавливается -> shutdown
except StopIteration:
pass
print("после остановки ready =", state["ready"])
Попробуй сам ▶ Между STARTUP и SHUTDOWN пул переиспользуется всеми запросами — в этом и смысл: дорогой ресурс создаётся один раз.
Жизненный цикл приложения целиком
Полезно держать в голове всю временную ось процесса: lifespan очерчивает её границы, а запросы живут внутри.
старт процесса
|
v
[ lifespan: код до yield ] <- один раз: пул БД, кэш, модели
|
v
+----------------------------------------+
| приём запросов (минуты/часы/дни) |
| запрос 1 -> обработчик -> ответ |
| запрос 2 -> обработчик -> ответ |
| ... (общие ресурсы переиспользуются)|
+----------------------------------------+
|
v
[ lifespan: код после yield ] <- один раз: закрыть пул, кэш
|
v
остановка процесса
Из этой картины видно ключевое различие двух масштабов жизни: ресурсы из lifespan живут всю вертикальную ось процесса, а сессии и зависимости с yield — лишь внутри одного горизонтального «запрос → ответ». Спутать эти масштабы — значит либо пересоздавать дорогой пул на каждый запрос, либо делить одну сессию между параллельными запросами.
Частые ошибки
Первая — продолжать использовать устаревшие @app.on_event в новом коде. Вторая — открывать дорогие ресурсы (пул БД, HTTP-клиент) внутри обработчиков или зависимостей на каждый запрос вместо lifespan. Третья — забывать закрывать ресурсы после yield, оставляя «висящие» соединения при перезапуске. Четвёртая — класть в lifespan логику, специфичную для запроса (ей место в зависимостях).
Best practices
- Используйте
lifespanвместо устаревшихon_event-декораторов. - Создавайте долгоживущие ресурсы (пулы, клиенты, модели) при старте и закрывайте при остановке.
- Храните общие ресурсы в
app.stateи доставайте их через зависимости. - Держите startup быстрым; тяжёлую инициализацию делайте осознанно и с логированием.
Состояние приложения и доступ из зависимостей
Ресурсы, созданные в lifespan, кладут в app.state — простое пространство имён на уровне приложения. Но как обработчику до них дотянуться? Через объект запроса: request.app.state.db_pool, или, элегантнее, через зависимость, которая достаёт ресурс из состояния и отдаёт его. Так замыкается красивая картина: lifespan создаёт пул один раз, кладёт в state, а зависимость на каждый запрос достаёт из пула отдельную сессию. Каждый слой отвечает за свой масштаб жизни. Важная тонкость: при нескольких воркерах lifespan выполняется в каждом процессе-воркере отдельно, поэтому пул создаётся не один на сервис, а один на воркер — это нормально и нужно учитывать при расчёте лимитов соединений к базе. Понимание этого избавляет от загадок вроде «почему соединений к базе в N раз больше, чем я ожидал».
Итог: lifespan — единый async-контекстменеджер для старта и остановки приложения, заменивший on_event. Он создаёт и закрывает общие ресурсы один раз за жизнь процесса; запросы пользуются ими между yield.