Модуль 25

Celery у FastAPI-проєкті

Ось готовий урок, створений у стилі CS50 — енергійний, з аналогіями та чітким фокусом на розумінні "навіщо".


🎓 Тема уроку: Celery у FastAPI-проєкті. Як не змушувати користувача чекати?

Привіт, друзі! Радий бачити вас на цьому занятті.

Сьогодні ми поговоримо про магію, яка відбувається за лаштунками веб-розробки. Ми навчимося робити ваші API не просто швидкими, а блискавичними, навіть коли їм потрібно виконати важку роботу.

Готові? Поїхали! 🚀


1. 🔥 Вступ: проблема та мотивація

Уявіть, що ви прийшли в популярну кав'ярню (Starbucks або вашу улюблену місцеву точку). Ви підходите до касира і замовляєте складний лате з кокосовим молоком і сиропом.

Запитання до вас: Що робить касир після того, як прийняв ваше замовлення? 1. Він сам іде молоти каву, гріти молоко, малювати серце на пінці, поки ви (і вся черга за вами) чекаєте біля каси? 2. Чи він просто кричить баристі: "Один кокосовий лате!", видає вам чек і одразу приймає замовлення у наступної людини?

Очевидно, варіант №2, правда? Бо якби касир готував кожну каву сам, черга рухалася б зі швидкістю черепахи.

А тепер подивіться на ваш код у FastAPI. Коли користувач реєструється на сайті, ви часто хочете: * Відправити привітальний email. * Стиснути завантажене фото профілю. * Згенерувати PDF-звіт.

Якщо ви робите це прямо всередині функції-обробника (path operation function), ваш FastAPI — це той самий "поганий касир". Користувач натискає "Реєстрація" і бачить кружечок завантаження 5... 10... 15 секунд. Він думає, що сайт зламався, і йде.

Нам це не підходить. Нам потрібен спосіб прийняти запит миттєво ("Замовлення прийнято!"), а важку роботу (відправку пошти, обробку відео) доручити комусь іншому, хто зробить це у фоновому режимі.

Саме тут на сцену виходить Celery.


2. 🧠 Теоретична база (без нудних лекцій)

Давайте розберемося, як ця "кухня" влаштована. Тут є три головні дійові особи.

1. Producer (Виробник) — Ваш FastAPI

Це наш касир. Його задача — швидко прийняти дані від користувача і створити "тікет" на виконання задачі. Він не виконує задачу сам!

2. Broker (Брокер) — Redis або RabbitMQ

Це дошка із замовленнями на кухні. FastAPI кидає туди записку: "Треба відправити імейл користувачу X". Брокер — це просто черга повідомлень. Він зберігає задачі, поки їх хтось не забере. Найчастіше ми використовуємо Redis (уявіть його як дуже швидку оперативну пам'ять для зберігання списків).

3. Consumer (Споживач) — Celery Worker

Це бариста (або кухар). Це окремий процес (програма), який запущений паралельно з вашим сайтом. Він постійно дивиться на Брокера: "Гей, є нові задачі?". Як тільки з'являється задача, він хапає її і починає працювати.

Що треба запам'ятати (інтуїтивно):

FastAPI і Celery — це дві різні програми. Вони можуть навіть працювати на різних серверах. Вони не спілкуються напряму, вони спілкуються через посередника (Брокера/Redis).


3. 🧪 Приклади (від простого до реального)

Припустімо, у нас вже встановлені бібліотеки: pip install fastapi uvicorn celery redis (І у вас запущений Redis локально або в Docker).

Крок 1: Створюємо "Кухаря" (Celery App)

Створимо файл celery_worker.py. Це налаштування нашого фонового виконавця.

# celery_worker.py
from time import sleep
from celery import Celery

# Налаштовуємо Celery. 
# 'broker' — це адреса, куди ми будемо кидати задачі (наш Redis).
# 'backend' — це місце, куди Celery покладе результат виконання (теж Redis).
celery_app = Celery(
    'worker',
    broker='redis://localhost:6379/0',
    backend='redis://localhost:6379/0'
)

@celery_app.task(name="heavy_task")  # Декоратор перетворює функцію на задачу Celery
def heavy_task(email: str):
    print(f"📩 Починаю відправку листа на {email}...")
    sleep(10)  # Імітуємо довгу роботу (10 секунд!)
    print(f"✅ Лист на {email} успішно відправлено!")
    return "Email sent"

Крок 2: Створюємо "Касира" (FastAPI)

Тепер файл main.py.

# main.py
from fastapi import FastAPI
from celery_worker import heavy_task  # Імпортуємо саму задачу

app = FastAPI()

@app.post("/register")
def register_user(email: str):
    # УВАГА: Ми не викликаємо heavy_task(email) напряму!
    # Ми викликаємо .delay() — це команда "поклади це в чергу".

    task = heavy_task.delay(email) 

    return {
        "message": "Реєстрація майже завершена! Лист прийде скоро.",
        "task_id": task.id,
        "status": "Processing"
    }

🤔 Чого ви очікуєте, коли ми зробимо POST-запит? Чи будемо ми чекати 10 секунд?

Відповідь: Ні! Відповідь прийде за мілісекунди. FastAPI скаже Redis-у: "Зроби це", і одразу поверне вам 200 OK. А в цей час у сусідньому вікні терміналу Celery прокинеться і почне "спати" 10 секунд.

Як це запустити? (Магія в терміналі)

Вам потрібно два термінали.

  1. Термінал 1 (FastAPI): uvicorn main:app --reload
  2. Термінал 2 (Celery Worker): celery -A celery_worker.celery_app worker --loglevel=info

4. 🛠 Практична частина

Тепер ваша черга закатати рукави!

Завдання 1: Hello World Скопіюйте код вище. Запустіть Redis, Worker та FastAPI. Зробіть запит через Swagger UI (/docs). Переконайтеся, що API відповів миттєво, а в логах Worker-а через 10 секунд з'явився напис "Лист відправлено".

Завдання 2: Математик Створіть нову Celery-задачу add(x, y), яка повертає суму двох чисел. Додайте ендпоінт у FastAPI /calculate, який приймає два числа. Підказка: Використайте task = add.delay(x, y).

Завдання 3: Перевірка статусу (Challenge 🌶️) У вас є task_id (ми повертали його в прикладі). Як користувачу дізнатися, чи завершилась задача? Створіть ендпоінт GET /status/{task_id}. Підказка: Вам знадобиться AsyncResult з бібліотеки celery.result.

from celery.result import AsyncResult
res = AsyncResult(task_id, app=celery_app)
print(res.status) # PENDING, STARTED, SUCCESS

Завдання 4: А що, якщо помилка? Змініть задачу так, щоб вона ділила на нуль. Подивіться, що буде в логах Worker-а. Чи впаде сам FastAPI? (Спойлер: Ні, і це чудово!)


5. 💡 Мислення як у розробника

Ось де різниця між новачком і профі. Як не вистрілити собі в ногу з Celery?

1. Не передавайте об'єкти бази даних (ORM) у задачу!

Погано: process_user.delay(user_orm_object) Чому? Поки задача дійде до виконання в черзі, дані в базі можуть змінитись. А ще об'єкт може бути складним для серіалізації (перетворення в текст для Redis).

Добре: process_user.delay(user_id=15) Передавайте тільки ID. Нехай Worker сам дістане свіжі дані з бази за цим ID.

2. Задачі мають бути ідемпотентними

Це страшне слово означає просту річ: якщо задачу виконати двічі, нічого поганого не станеться. Іноді мережа глючить, і Celery може отримати одну й ту ж задачу двічі. Погано: Задача "Зняти 100 грн з рахунку". (Два рази = зняли 200 грн). Добре: Задача "Встановити баланс = 500 грн". (Хоч 10 разів зроби — баланс буде 500).

3. Моніторинг

Як ви дізнаєтесь, що черга забита? Досвідчені розробники використовують Flower — це веб-інтерфейс для Celery. Встановіть pip install flower і запустіть, щоб бачити графіки виконання задач.


6. 🧩 Підсумок

Друзі, сьогодні ми розблокували суперсилу асинхронності.

  1. FastAPI — це обличчя (приймає замовлення).
  2. Redis — це блокнот (черга).
  3. Celery — це руки (виконує важку роботу).

Що ви тепер вмієте? Ви можете будувати додатки, які не "виснуть", коли треба обробити відео, надіслати тисячу листів або згенерувати складний звіт. Ви поважаєте час своїх користувачів.

Що далі? Наступного разу ми поговоримо про Celery Beat. Це як будильник для ваших задач. Хочете відправляти дайджест новин щоранку о 9:00 автоматично? Celery Beat це зробить.

А поки — експериментуйте з кодом! Удачі! 💻🔥