Модуль 24

RabbitMQ у FastAPI-проєктах

Ось готовий урок, створений спеціально для тебе у стилі CS50. Вмикай уяву, ми починаємо!


🎓 CS50-Style: RabbitMQ у FastAPI-проєктах

Привіт, друзі! Мене звати [Твоє Ім'я], і сьогодні ми поговоримо про те, як не змушувати ваших користувачів чекати.

1. 🔥 Вступ: Чому все так повільно?

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

Риторичне питання: Що станеться з чергою? Вона розтягнеться до вулиці! Люди будуть лютувати. Бізнес втратить гроші.

А тепер подивіться на ваш FastAPI-додаток. Користувач натискає кнопку «Зареєструватися». Ваш сервер починає: 1. Зберігати дані в БД (швидко). 2. Генерувати PDF-звіт (довго). 3. Відправляти вітальний email через зовнішній SMTP (дуже довго).

Поки все це відбувається, браузер користувача «крутиться». Він чекає. Якщо це займе 10 секунд — користувач піде.

У чому проблема? Ви робите все це синхронно (або навіть в одному event loop, блокуючи його логікою CPU). Рішення: Нам потрібен «бармен» (Worker), який робить каву, поки «касир» (FastAPI) просто приймає замовлення і видає чеки.

Цей механізм передачі замовлення від касира до бармена — це і є Брокер Повідомлень. І сьогодні наш герой — RabbitMQ.


2. 🧠 Теоретична база: Як це працює «під капотом»

Давайте розберемо це без складних слів. RabbitMQ — це, по суті, поштове відділення.

У нас є 4 головні дійові особи:

  1. Producer (Продюсер): Це ваш FastAPI. Його робота — лише створити повідомлення («Треба відправити лист користувачу X») і кинути його в скриньку. Він не чекає, поки лист дійде!
  2. Exchange (Обмінник): Це сортувальний центр. Він вирішує, у яку саме чергу покласти повідомлення (про це пізніше, поки уявіть, що він просто передає далі).
  3. Queue (Черга): Це власне поштова скринька. Буфер, де лежать повідомлення і чекають.
  4. Consumer (Консюмер / Воркер): Це окремий скрипт (працівник), який постійно моніторить чергу. Щойно там щось з'являється — він хапає це і починає працювати (відправляє email, обробляє відео тощо).

🔑 Що треба запам'ятати залізно:

  • Decoupling (Розчеплення): FastAPI не знає, хто і коли обробить задачу. Йому байдуже. Його діло — швидко відповісти клієнту «202 Accepted» (Задачу прийнято).
  • ACK (Підтвердження): Коли Воркер забрав задачу, він має сказати RabbitMQ: «Я все зробив, можеш видаляти». Якщо Воркер впаде під час роботи і не дасть ACK — RabbitMQ поверне задачу в чергу для іншого Воркера. Надійність!

3. 🧪 Приклади: Від теорії до коду

Для роботи нам знадобиться бібліотека aio-pika (вона асинхронна, ідеальна для FastAPI).

🔹 Крок 1: Встановлення

pip install fastapi uvicorn aio-pika
# Також переконайтеся, що у вас запущений RabbitMQ (наприклад, через Docker):
# docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:management

🔹 Крок 2: Продюсер (FastAPI)

Ми не будемо відправляти email, ми просто скажемо RabbitMQ: "Зроби це".

main.py

from fastapi import FastAPI
import aio_pika
import json

app = FastAPI()

# Підключення до RabbitMQ (спрощено)
async def get_rabbitmq_channel():
    connection = await aio_pika.connect_robust("amqp://guest:guest@localhost/")
    return await connection.channel()

@app.post("/send-email/")
async def send_welcome_email(email: str):
    channel = await get_rabbitmq_channel()

    # Тіло повідомлення
    message_body = json.dumps({"type": "welcome_email", "email": email})

    # Відправляємо в чергу "email_queue"
    await channel.default_exchange.publish(
        aio_pika.Message(body=message_body.encode()),
        routing_key="email_queue"
    )

    return {"message": "Задачу на відправку пошти створено! Ми це зробимо фоново."}

Що тут відбулося? Ви натиснули "Send". Сервер відповів миттєво. Але пошта ще не пішла. Лист лежить у RabbitMQ.


🔹 Крок 3: Консюмер (Воркер)

Це окремий Python-файл. Він буде запущений в іншому терміналі. Він імітує важку роботу.

worker.py

import asyncio
import aio_pika
import json
import time

async def process_message(message: aio_pika.IncomingMessage):
    async with message.process(): # Це автоматично відправить ACK, якщо код виконається без помилок
        data = json.loads(message.body.decode())
        print(f" [x] Отримано задачу: {data}")

        # Імітуємо важку роботу (відправка пошти)
        print(f" [⏳] Відправляю лист на {data['email']}...")
        await asyncio.sleep(5) # Спимо 5 секунд

        print(f" [✅] Лист успішно відправлено!")

async def main():
    connection = await aio_pika.connect_robust("amqp://guest:guest@localhost/")
    channel = await connection.channel()

    # Оголошуємо ту ж саму чергу
    queue = await channel.declare_queue("email_queue")

    print(" [*] Чекаю на повідомлення. Натисніть CTRL+C для виходу")

    # Слухаємо чергу
    await queue.consume(process_message)

    # Тримаємо скрипт запущеним
    await asyncio.Future()

if __name__ == "__main__":
    asyncio.run(main())

Очікування студента: Коли ми запустимо worker.py, він нічого не робитиме. Реальність: Як тільки ми зробимо запит до FastAPI, воркер "прокинеться", напише "Отримано задачу", почекає 5 секунд і скаже "Готово".


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

Час забруднити руки! Відкрийте два термінали. В одному — uvicorn main:app --reload, в іншому — python worker.py.

  1. Завдання 1 (Базове): Зробіть POST-запит на /send-email/ з вашою поштою. Переконайтеся, що FastAPI відповів миттєво, а Worker почав "думати".
  2. Завдання 2 (Навантаження): Швидко відправте 5 запитів підряд. Подивіться на лог Воркера. Він обробляє їх по черзі (один за одним). Черга працює!
  3. Завдання 3 (Масштабування): Запустіть ще один термінал з python worker.py (тепер у вас два воркера). Знову відправте 5-10 запитів.
    • Питання: Як розподілилися задачі? (RabbitMQ використовує Round-Robin — по колу: тобі, тобі, тобі...).
  4. Завдання 4 (Помилка): У файлі worker.py перед await asyncio.sleep додайте raise Exception("Пошта впала!"). Запустіть.
    • Що сталося? Повідомлення не зникло назавжди? Чи спробував RabbitMQ віддати його знову? (Якщо message.process() не завершився успішно, повідомлення повертається в чергу).
  5. Міні-кейс: Створіть новий ендпоінт /resize-image/, який приймає назву файлу. Воркер має писати: "Змінюю розмір картинки..." і чекати 2 секунди.

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

Ви тепер вмієте перекидати JSON-и. Але як думає Senior Developer?

⚠️ Типова помилка новачка: "Передача слона"

Ніколи не передавайте сам файл (картинку, PDF) через RabbitMQ. Повідомлення мають бути маленькими (до кількох кілобайт). * Погано: { "image_data": "<base64_string_5mb>" } * Добре: { "image_path": "/s3/bucket/avatar_123.jpg", "user_id": 55 } Воркер отримає шлях, сам скачає файл зі сховища, обробить і збереже назад.

⚠️ Типова помилка №2: Забутий Connection

У прикладі вище ми створюємо з'єднання при кожному запиті. Це дорого! * Як профі: У FastAPI використовують події startup та shutdown, щоб відкрити з'єднання один раз при запуску сервера і використовувати його скрізь (або Dependency Injection).

🧩 Ідемпотентність

Складне слово, проста суть: "Що буде, якщо одну задачу виконати двічі?" Іноді мережа глючить, і RabbitMQ може віддати задачу вдруге. Ваш воркер має бути готовим до цього. Наприклад, перевіряти в БД: "А чи не відправив я вже цей лист 5 хвилин тому?".


6. 🧩 Підсумок

Що ми сьогодні зробили? Ми розірвали жорсткий зв'язок між прийомом замовлення (HTTP Request) і його виконанням. Тепер ваш FastAPI може обробляти тисячі запитів на секунду, навіть якщо відправка пошти займає хвилину. Ви просто додаєте більше воркерів у фоні, не чіпаючи основний сайт.

Ви вмієте: 1. Запускати RabbitMQ. 2. Відправляти повідомлення з FastAPI (aio-pika). 3. Писати фонові воркери, які розгрібають ці завали.

Тизер наступного уроку: А що, якщо нам треба, щоб воркер повернув результат назад у FastAPI (наприклад, згенерований текст від ChatGPT)? RabbitMQ — це дорога в один кінець? Ні! На наступному уроці розберемо RPC (Remote Procedure Call) патерн.

А поки що — це був CS50. (Стук коду по клавішах). Успіхів!