You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

12 KiB

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

{* ../../docs_src/events/tutorial003_py310.py hl[16,19] *}

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

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

/// tip | Порада

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

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

///

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

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

{* ../../docs_src/events/tutorial003_py310.py hl[14:19] *}

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

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

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

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

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

{* ../../docs_src/events/tutorial003_py310.py hl[1,13] *}

Менеджер контексту в 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.

{* ../../docs_src/events/tutorial003_py310.py hl[22] *}

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

/// warning | Попередження

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

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

///

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

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

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

Подія startup

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

{* ../../docs_src/events/tutorial001_py310.py hl[8] *}

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

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

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

Подія shutdown

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

{* ../../docs_src/events/tutorial002_py310.py hl[6] *}

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

/// info | Інформація

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

///

/// tip | Порада

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

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

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

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

///

Разом startup і shutdown

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

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

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

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

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

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

/// info | Інформація

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

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

///

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

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