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

Події тривалості життя

🌐 Переклад ШІ та людьми

Цей переклад виконано ШІ під керівництвом людей. 🤝

Можливі помилки через неправильне розуміння початкового змісту або неприродні формулювання тощо. 🤖

Ви можете покращити цей переклад, допомігши нам краще спрямовувати AI LLM.

Англійська версія

Ви можете визначити логіку (код), яку слід виконати перед тим, як застосунок запуститься. Це означає, що цей код буде виконано один раз, перед тим як застосунок почне отримувати запити.

Так само ви можете визначити логіку (код), яку слід виконати під час вимкнення застосунку. У цьому випадку код буде виконано один раз, після обробки можливо багатьох запитів.

Оскільки цей код виконується перед тим, як застосунок почне приймати запити, і одразу після того, як він завершить їх обробку, він охоплює всю тривалість життя застосунку (слово «lifespan» буде важливим за мить 😉).

Це дуже корисно для налаштування ресурсів, які потрібні для всього застосунку, які спільні між запитами, та/або які потрібно потім прибрати. Наприклад, пул з’єднань з базою даних або завантаження спільної моделі машинного навчання.

Випадок використання

Почнемо з прикладу випадку використання, а потім подивимось, як це вирішити.

Уявімо, що у вас є моделі машинного навчання, якими ви хочете обробляти запити. 🤖

Ті самі моделі спільні між запитами, тобто це не окрема модель на запит чи на користувача.

Уявімо, що завантаження моделі може займати чимало часу, бо треба читати багато даних з диска. Тож ви не хочете робити це для кожного запиту.

Ви могли б завантажити її на верхньому рівні модуля/файлу, але це означало б, що модель завантажиться навіть якщо ви просто запускаєте простий автоматизований тест - тоді тест буде повільним, бо йому доведеться чекати завантаження моделі перед виконанням незалежної частини коду.

Ось це ми й вирішимо: завантажимо модель перед обробкою запитів, але лише безпосередньо перед тим, як застосунок почне отримувати запити, а не під час завантаження коду.

Тривалість життя

Ви можете визначити цю логіку запуску і вимкнення за допомогою параметра lifespan застосунку FastAPI та «менеджера контексту» (зараз покажу, що це).

Почнемо з прикладу, а потім розберемо детально.

Ми створюємо асинхронну функцію lifespan() з yield так:

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}

Тут ми імітуємо дорогу операцію запуску із завантаженням моделі, поміщаючи (фальшиву) функцію моделі у словник з моделями машинного навчання перед yield. Цей код буде виконано перед тим, як застосунок почне приймати запити, під час запуску.

А одразу після yield ми розвантажуємо модель. Цей код буде виконано після того, як застосунок завершить обробку запитів, безпосередньо перед вимкненням. Це, наприклад, може звільнити ресурси на кшталт пам’яті або GPU.

Порада

Подія shutdown відбувається, коли ви зупиняєте застосунок.

Можливо, вам треба запустити нову версію, або ви просто втомилися її запускати. 🤷

Функція тривалості життя

Перше, на що слід звернути увагу: ми визначаємо асинхронну функцію з yield. Це дуже схоже на залежності з yield.

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}

Перша частина функції до yield буде виконана перед запуском застосунку.

А частина після yield буде виконана після завершення роботи застосунку.

Асинхронний менеджер контексту

Якщо придивитися, функція задекорована за допомогою @asynccontextmanager.

Це перетворює функцію на так званий «асинхронний менеджер контексту».

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}

Менеджер контексту в Python - це те, що можна використовувати в операторі with, наприклад, open() можна використовувати як менеджер контексту:

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

У новіших версіях Python також є асинхронний менеджер контексту. Його використовують з async with:

async with lifespan(app):
    await do_stuff()

Коли ви створюєте менеджер контексту або асинхронний менеджер контексту, як вище, перед входом у блок with буде виконано код перед yield, а після виходу з блоку with буде виконано код після yield.

У нашому прикладі коду вище ми не використовуємо його напряму, а передаємо його до FastAPI, щоб він його використав.

Параметр lifespan застосунку FastAPI приймає асинхронний менеджер контексту, тож ми можемо передати йому наш новий асинхронний менеджер контексту lifespan.

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}

Альтернативні події (застаріло)

Попередження

Рекомендований спосіб обробляти запуск і вимкнення - використовувати параметр lifespan застосунку FastAPI, як описано вище. Якщо ви надаєте параметр lifespan, обробники подій startup і shutdown більше не будуть викликані. Або все через lifespan, або все через події - не обидва одночасно.

Можете, ймовірно, пропустити цю частину.

Є альтернативний спосіб визначити логіку, яку слід виконати під час запуску і під час вимкнення.

Ви можете визначити обробники подій (функції), які потрібно виконати перед запуском застосунку або коли застосунок вимикається.

Ці функції можна оголошувати як з async def, так і звичайним def.

Подія startup

Щоб додати функцію, яку слід виконати перед запуском застосунку, оголосіть її з подією "startup":

from fastapi import FastAPI

app = FastAPI()

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]

У цьому випадку функція-обробник події startup ініціалізує «базу даних» предметів (це лише dict) деякими значеннями.

Ви можете додати більше ніж один обробник події.

І ваш застосунок не почне приймати запити, доки всі обробники події startup не завершаться.

Подія shutdown

Щоб додати функцію, яку слід виконати, коли застосунок вимикається, оголосіть її з подією "shutdown":

from fastapi import FastAPI

app = FastAPI()


@app.on_event("shutdown")
def shutdown_event():
    with open("log.txt", mode="a") as log:
        log.write("Application shutdown")


@app.get("/items/")
async def read_items():
    return [{"name": "Foo"}]

Тут функція-обробник події shutdown запише текстовий рядок "Application shutdown" у файл log.txt.

Інформація

У функції open() параметр mode="a" означає «append», тож рядок буде додано після всього, що є у файлі, без перезапису попереднього вмісту.

Порада

Зауважте, що в цьому випадку ми використовуємо стандартну Python-функцію open(), яка працює з файлом.

Тобто вона включає I/O (input/output), де потрібно «чекати», поки дані буде записано на диск.

Але open() не використовує async і await.

Тому ми оголошуємо функцію-обробник події зі стандартним def, а не async def.

Разом startup і shutdown

Велика ймовірність, що логіка для вашого запуску і вимкнення пов’язана: ви можете хотіти щось запустити, а потім завершити, отримати ресурс, а потім звільнити його тощо.

Робити це в окремих функціях, які не діляться логікою чи змінними, складніше - доведеться зберігати значення у глобальних змінних або вдаватися до подібних трюків.

Тому зараз рекомендується натомість використовувати lifespan, як пояснено вище.

Технічні деталі

Невелика технічна деталь для допитливих нердів. 🤓

Під капотом, у технічній специфікації ASGI, це частина Протоколу тривалості життя, і там визначені події startup і shutdown.

Інформація

Ви можете прочитати більше про обробники lifespan у документації Starlette про Lifespan.

Зокрема, як працювати зі станом тривалості життя, який можна використовувати в інших ділянках вашого коду.

Підзастосунки

🚨 Майте на увазі, що ці події тривалості життя (startup і shutdown) виконуються лише для головного застосунку, а не для Підзастосунки - монтування.