Перейти до змісту

Життєвий цикл застосунку

Частина роботи виконується один раз на процес, а не один раз на запит: відкриття пулу з'єднань бази даних, створення спільного HTTP-клієнта, завантаження ML-моделі чи файлу конфігурації в пам'ять — і охайне згортання цього на виході. FastAPI кладе це у lifespan — асинхронний контекстний менеджер, прив'язаний до запуску та зупинки сервера.

Контекстний менеджер lifespan

Напишіть функцію з @asynccontextmanager, яка приймає застосунок. Усе до yield виконується один раз при запуску, перш ніж обслужиться будь-який запит; усе після yield виконується один раз при зупинці, після останнього запиту. Передайте її у FastAPI(lifespan=...):

from contextlib import asynccontextmanager

from fastapi import FastAPI, Request

from app.db import FakePool


@asynccontextmanager
async def lifespan(app: FastAPI):
    app.state.pool = FakePool()   # startup: open the resource
    yield
    app.state.pool.close()        # shutdown: close it


app = FastAPI(lifespan=lifespan)


@app.get("/users")
def list_users(request: Request):
    return {"users": request.app.state.pool.get_users()}

Ресурс живе на app.state — простому просторі імен для об'єктів рівня застосунку — а ендпоінти дістаються до нього через request.app.state. Запустіть застосунок, і Uvicorn запише в лог Application startup complete, щойно виконається половина зі стартом; на Ctrl-C він виконує половину із зупинкою перед виходом.

Оскільки це єдиний контекстний менеджер, налаштування та згортання стоять поруч і мають спільну область видимості — форма «отримати, yield, звільнити» (try/finally), для якої й створені контекстні менеджери Python.

Note

Старіші декоратори @app.on_event("startup") / @app.on_event("shutdown") ще існують, але вважаються застарілими. Для нового коду використовуйте lifespan.

Доступ до ресурсу

Два способи скористатися тим, що створив lifespan:

  • request.app.state.pool — напряму, як вище.
  • Залежність (dependency), яка повертає request.app.state.pool, щоб ендпоінти просили pool у своїй сигнатурі, а не діставалися крізь запит. Це ідіоматичний підхід, і йому присвячено окрему тему (Впровадження залежностей).

З досвіду Django: Найближче, що дає вам Django, — це AppConfig.ready(): метод, який виконується один раз, коли реєстр застосунків завершує завантаження. Це місце для реєстрації сигналів, стартових перевірок чи прогріву кешу. Дві різниці мають значення:

Він лише для запуску. У ready() немає відповідника для зупинки — Django не має вбудованого хука завершення процесу для вашого коду. lifespan у FastAPI симетричний: та сама функція володіє обома кінцями, тож те, що ви відкрили, ви ж і закриваєте.

Він виконується майже всюди. ready() спрацьовує для кожної точки входу, яка завантажує реєстр застосунків — runserver, але також кожна команда manage.py, міграції та shell. Тому вас застерігають не робити важку роботу (як-от відкриття реальних з'єднань) у ready() без захисту. lifespan у FastAPI виконується лише тоді, коли ASGI-застосунок насправді обслуговується Uvicorn — а не коли ви імпортуєте модуль чи запускаєте разовий скрипт — тож «відкрити тут пул з'єднань» — це саме те, для чого він є.

Супровідні проєкти роблять асиметрію відчутною

Проєкт FastAPI відкриває пул при запуску й закриває його при зупинці, і його тест перевіряє обидві половини. Проєкт Django відкриває той самий пул у AppConfig.ready() і не має кроку зупинки — бо його нікуди подіти. Той самий стартовий ресурс, але лише один фреймворк дає вам відповідне згортання.

Зауваження щодо тестування

TestClient виконує lifespan лише коли використовується як контекстний менеджер:

with TestClient(app) as client:   # startup runs here
    ...                           # make requests
# shutdown runs here, on exit

client = TestClient(app) на рівні модуля повністю пропускає lifespan — тож ендпоінт, що покладається на app.state.pool, впаде. Це збиває людей з пантелику; коли ваші тести торкаються стану, створеного в lifespan, використовуйте форму with.

Супровідні проєкти

У каталозі 05-demo/ цього розділу:

  • 05-demo/fastapi/ (завантажити) — lifespan, що відкриває й закриває пул; тест перевіряє «відкрито під час» і «закрито після».
  • 05-demo/django/ (завантажити) — AppConfig.ready() відкриває пул; тест перевіряє, що він відкритий при запуску (зупинку тестувати нічим).

FastAPI: pytest (2 тести). Django: manage.py test (3 тести).

Джерела