Модуль 38

Celery як частина distributed system

Ось готовий урок, згенерований за твоїм майстер-промптом.


🎓 Тема: Celery як частина distributed system

(або: Як перестати змушувати користувача чекати)


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

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

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

Звучить як бізнес, що збанкрутує за тиждень, правда?

Але саме так працює більшість веб-додатків "з коробки"! Коли користувач натискає кнопку "Згенерувати звіт" або "Обробити відео", сервер (наш касир) кидає все і починає це робити. Браузер "висне", користувач бачить кружечок завантаження... 5 секунд, 10, 30... Timeout Error. Користувач йде геть.

Питання до вас: Чи готові ви чекати хвилину на сайті, поки вам на пошту відправиться лист підтвердження реєстрації? Звісно ні. Ви хочете натиснути "ОК" і миттєво побачити "Лист відправлено".

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

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


2. 🧠 Теоретична база (без сухої академічності)

Давайте розберемо архітектуру. Celery — це не просто бібліотека, це інструмент для побудови розподіленої черги задач (Distributed Task Queue).

Уявіть кухню ресторану:

  1. Producer (Продюсер/Веб-сервер): Офіціант. Він лише приймає замовлення і клеїть папірець на спеціальну дошку. Він не готує.
  2. Broker (Брокер): Та сама дошка з папірцями. Це посередник. Офіціант кладе туди задачу, кухар звідти її забирає. Найпопулярніші брокери — Redis або RabbitMQ. Без брокера Celery не працює, бо продюсер і виконавці не знають про існування одне одного.
  3. Worker (Воркер/Робітник): Кухар. Це процес Celery, який висить у фоні, дивиться на брокера (дошку) і, як тільки там щось з’являється, хапає задачу і виконує її.
  4. Result Backend: Журнал видачі. Куди записати результат ("Страва готова" або "Помилка: закінчилось молоко").

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

  • Ваш веб-сайт (Django/Flask/FastAPI) і Celery Worker — це різні процеси. Вони можуть бути навіть на різних серверах.
  • Вони спілкуються тільки через Брокер (Redis).
  • Ця система асинхронна. Ви не знаєте точно, коли задача виконається — за секунду чи за годину.

💡 Інтуїтивне розуміння:

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


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

Приклад №1: Hello World (або 2 + 2)

Припустимо, у нас є функція, яка додає два числа.

Без Celery (Синхронно):

def add(x, y):
    return x + y

# Веб-запит висить тут, поки функція рахує
result = add(4, 4) 
print(result)

З Celery (Асинхронно): Спершу налаштуємо Celery (уяви, що Redis вже запущено):

from celery import Celery

# Створюємо екземпляр Celery
app = Celery('tasks', broker='redis://localhost:6379/0')

@app.task
def add(x, y):
    return x + y

Як ми це викликаємо? Студенте, як думаєш, якщо я викличу add(4, 4), воно виконається у фоні? Ні! Це буде звичайний виклик функції. Щоб відправити її в Celery, треба магія.

# Правильний виклик
result = add.delay(4, 4) 

Що відбувається в цей момент: 1. Python не рахує 4+4. 2. Він створює JSON повідомлення: {"task": "add", "args": [4, 4]}. 3. Відправляє його в Redis. 4. Метод .delay() миттєво повертає ID задачі (наприклад, b185-32...). 5. Десь в іншому терміналі запущений Worker бачить це повідомлення, рахує і кладе результат назад.


Приклад №2: Реальне життя (Відправка Email)

Типова задача: користувач зареєструвався.

import time

@app.task
def send_welcome_email(email):
    # Імітуємо повільний SMTP сервер
    print(f"Connecting to SMTP server for {email}...")
    time.sleep(5)  # Це заблокувало б сайт на 5 секунд!
    print(f"Email sent to {email}!")
    return "OK"

У контролері сайту (view):

def register_user(request):
    # ... зберігаємо юзера в БД ...

    # Відправляємо задачу в чергу і миттєво відповідаємо юзеру
    send_welcome_email.delay("user@example.com")

    return "Ви зареєстровані! Перевірте пошту (може зайняти хвилинку)."

Користувач щасливий, бо сторінка завантажилась за 0.1 сек. Воркер щасливий, бо має роботу.


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

Час "забруднити руки". Для цього тобі знадобиться Python і Redis (можна запустити в Docker).

Завдання 1: Запуск Створи файл tasks.py з прикладом додавання (add). Запусти воркер командою в терміналі: celery -A tasks worker --loglevel=info В іншому терміналі відкрий python і виклич add.delay(10, 20). Подивись у логи воркера.

Завдання 2: Ефект сповільнення Додай у функцію add time.sleep(10). Виклич задачу знову. Переконайся, що твій скрипт (де ти викликав .delay()) не зачекав 10 секунд, а завершився миттєво. А воркер почав "думати".

Завдання 3: Саботаж (А що, якщо...) Зупини воркер (Ctrl+C). Але не зупиняй Redis. Виклич задачу add.delay(5, 5) кілька разів. Питання: Де ділися задачі? Вони зникли? Відповідь: Запусти воркер знову і подивись, що станеться. (Спойлер: вони посипляться як з відра, бо лежали в черзі Redis).

Завдання 4: Міні-кейс Напиши задачу process_image(image_path), яка імітує зміну розміру фото (просто sleep(3) і print). Зроби скрипт, який імітує завантаження галереї з 10 фото. Запусти цикл, який відправляє 10 задач у Celery. Подивись, як вони обробляються (послідовно чи паралельно? Підказка: залежить від налаштувань воркера).


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

Окей, синтаксис — це просто. Складніше — архітектура. Ось де новачки ламають ноги.

❌ Типова помилка новачка: Передача об'єктів

# НІКОЛИ ТАК НЕ РОБИ
user = User.objects.get(id=1)
send_email.delay(user) 

Чому це погано? 1. Поки задача дійде до воркера (через 10 хв), дані юзера в базі можуть змінитись. Воркер працюватиме зі старою копією. 2. Об'єкт треба серіалізувати (перетворити в текст для Redis). Складні об'єкти часто не серіалізуються.

✅ Як думає профі:

# РОБИ ТАК
user_id = 1
send_email.delay(user_id)

Воркер отримає ID, сам піде в базу і дістане найсвіжіші дані. Передавай тільки "легкі" дані (ID, strings, numbers).

💡 Порада з практики:

Завжди пам’ятай про Idempotency (Ідемпотентність). Що, як воркер впаде посеред роботи, а Redis вирішить, що задача не зроблена, і віддасть її іншому воркеру? Задача виконається двічі. Якщо це "зписати гроші з картки" — у вас проблеми. Код має бути готовим до того, що задача може запуститися двічі.


6. 🧩 Підсумок

Сьогодні ми розділили наш додаток на дві частини: "Голова" (веб-сервер), яка думає швидко, і "Руки" (Celery), які роблять важку роботу.

Що ти тепер вмієш: 1. Розумієш різницю між Producer, Broker і Worker. 2. Вмієш створювати прості асинхронні задачі. 3. Знаєш, чому не можна передавати об'єкти бази даних у чергу.

Тизер наступного уроку: А що, якщо ми хочемо, щоб задача запускалася не коли юзер натисне кнопку, а кожного ранку о 9:00? Наприклад, розсилка новин? Нам не потрібен для цього окремий cron на сервері. Нам потрібен Celery Beat. Про це — наступного разу! 🚀