Модуль 18

Canvas: chains, groups, chords

Ось повноцінний урок у стилі David Malan (CS50), адаптований під українську аудиторію. Ми розбираємо тему Celery Canvas: chains, groups, chords.


🎓 Урок: Мистецтво керування потоками (Celery Canvas)

Привіт, друзі! 👋

Сьогодні ми переходимо від написання простого коду, який виконується "зверху вниз", до архітектури справжніх розподілених систем. Ми поговоримо про Canvas у бібліотеці Celery (Python).

Але спершу...


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

Уявіть, що ви розробляєте «Uber для доставки піци». Клієнт натискає кнопку "Замовити".

Що має статися далі? 1. Списати гроші з картки. 2. Відправити замовлення на кухню. 3. Знайти кур’єра. 4. Надіслати клієнту Push-повідомлення "Готуємо!". 5. Надіслати чек на пошту.

Питання до вас: Чи хочете ви, щоб клієнт дивився на кружечок завантаження в телефоні 10 секунд, поки ваш сервер намагається зробити все це в одному синхронному запиті?

Звісно, ні. Клієнт піде до конкурентів.

Ви скажете: "Окей, Девіде, ми вже знаємо про Celery — ми просто відправимо це в фонову задачу!".

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

Ось тут звичайного task.delay() замало. Вам потрібен план виконання. Вам потрібна оркестрація.

Сьогодні ми навчимося бути диригентами цього оркестру за допомогою трьох інструментів: Chain (Ланцюг), Group (Група) та Chord (Акорд).


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

У Celery є поняття Canvas (полотна). Це спосіб малювати складні робочі процеси (workflows).

Давайте розберемо три головні примітиви. Але спершу — важливий термін: Signature (Підпис).

Signature (.s()) — це коли ви упаковуєте задачу та її аргументи в коробку, але ще не відправляєте її. Це "заморожений" виклик функції.

🔗 1. Chain (Ланцюг)

Уявіть конвеєр на заводі. Логіка: Зроби А, результат А передай у Б, результат Б передай у В. Де потрібно: Обробка даних у кілька етапів (Завантажити файл -> Розархівувати -> Прочитати). Головне правило: Вихід попередньої задачі стає першим аргументом наступної.

🥞 2. Group (Група)

Уявіть, що ви зайшли в бар з 5 друзями і всі крикнули бармену: "Дай пива!". Бармен починає наливати всім одночасно (ну, або майже, якщо барменів кілька). Логіка: Запусти 10 задач паралельно. Нам байдуже, хто закінчить першим. Де потрібно: Відправити 1000 email-розсилок, спарсити ціни з 5 різних сайтів.

🎼 3. Chord (Акорд)

Це найцікавіше. У музиці акорд — це кілька нот, що звучать разом, створюючи гармонію. У Celery це комбінація Group + Callback. Логіка: "Зробіть ось ці 5 завдань паралельно (Header). Коли всі вони закінчаться, запустіть ось цю одну фінальну задачу (Body)". Де потрібно: Розрізати відео на 10 шматків, обробити кожен окремо, а потім склеїти назад. Без Chord ви б не знали, коли починати склеювання.


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

Припустімо, у нас є базові задачі (tasks):

from celery import shared_task

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

@shared_task
def double(x):
    return x * 2

@shared_task
def thank_user(results):
    # results — це список результатів від попередніх задач
    print(f"Все готово! Результати: {results}")
    return "Done"

Приклад 1: Chain (Послідовність)

Ми хочемо порахувати (4 + 4) * 2.

Що ви очікуєте побачити в коді? Ми маємо зв'язати add і double.

from celery import chain

# Створюємо підписи (signatures)
step1 = add.s(4, 4)  # Поверне 8
step2 = double.s()   # Прийме 8 як аргумент, поверне 16

# Запускаємо ланцюг
workflow = chain(step1, step2)
result = workflow.apply_async()

print(result.get()) # Виведе: 16

Чому так? add повернув 8. Celery автоматично взяв цю 8 і підставив її в double(8). Магія! ✨


Приклад 2: Group (Паралельність)

Ми хочемо порахувати 1+1, 2+2, 3+3 одночасно.

from celery import group

# Створюємо список задач
tasks = [add.s(i, i) for i in range(1, 4)]

# Запускаємо групу
job = group(tasks)
result = job.apply_async()

print(result.get()) 
# Що ми побачимо? 
# Відповідь: [2, 4, 6] (список результатів у тому ж порядку)

Приклад 3: Chord (Реальна магія)

Задача: Отримати дані про погоду з 3-х різних міст (паралельно), а потім порахувати середню температуру.

from celery import chord

@shared_task
def get_temp(city):
    # Імітація запиту до API
    temps = {"Kyiv": 20, "Lviv": 18, "Odesa": 22}
    return temps.get(city, 0)

@shared_task
def calculate_avg(temperatures):
    # temperatures прийде як [20, 18, 22]
    avg = sum(temperatures) / len(temperatures)
    return f"Середня температура: {avg}"

# Сценарій:
# 1. Header (Група): збираємо температури
header = [get_temp.s("Kyiv"), get_temp.s("Lviv"), get_temp.s("Odesa")]

# 2. Body (Callback): що робити в кінці
callback = calculate_avg.s()

# Запуск акорду
workflow = chord(header)(callback)
# АБО коротший синтаксис: chord(header, callback).apply_async()

Як це працює під капотом? Celery створює групу завдань. Коли останнє завдання з групи звітує про успіх, Celery тригерить calculate_avg і передає їй список результатів.


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

Тепер ваша черга. Не бійтеся помилятися — компілятор вас не вкусить.

Завдання 1: Математичний конвеєр (Chain) Створіть ланцюжок, який: 1. Бере число 5. 2. Додає 10. 3. Множить на 10. 4. Віднімає 1. Очікуваний результат: 149.

Завдання 2: Спам-машина (Group) У вас є список email-адрес: ['a@test.com', 'b@test.com', 'c@test.com']. Створіть групу задач send_email(email), яка виводить в консоль "Email sent to...". Запустіть їх паралельно.

Завдання 3: Аналіз продажів (Chord) Це вже серйозно. 1. Напишіть задачу fetch_daily_sales(day_of_week), яка повертає випадкове число (продажі). 2. Створіть акорд, який запускає цю задачу для 7 днів тижня (Mon-Sun). 3. Фінальна задача weekly_report(sales_list) має підсумувати суму і повернути "Тижневий виторг: $XXXX".

Завдання 4: Міні-кейс "Обробка зображень" Спроектуйте workflow (словесно або псевдокодом): Користувач завантажує ZIP-архів з фотографіями. Вам треба: 1. Розархівувати. 2. Кожне фото зменшити до 100x100px. 3. Накласти водяний знак на кожне фото. 4. Знову запакувати в ZIP. 5. Надіслати посилання користувачу.

Підказка: тут буде комбінація Chain і Chord.


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

Ви тепер знаєте синтаксис. Але як думає Senior Developer, працюючи з Canvas?

1. Передавайте ID, а не об'єкти

Помилка новачка: Передавати в chain цілий об'єкт користувача з бази даних (Django Model, SQLAlchemy Object). Чому це погано: Поки задача стоїть у черзі, дані в базі можуть змінитися. А ще серіалізація великих об'єктів забиває пам'ять брокера (Redis/RabbitMQ). Як треба: Передавайте user_id. Задача сама дістане свіжі дані з бази.

2. Результати (Result Backend) — це важливо

Для роботи Chord Celery мусить десь зберігати результати проміжних задач, щоб передати їх у фінальну. Якщо ви не налаштували result_backend (наприклад, Redis або Database), ваші акорди просто не спрацюють або зависнуть.

3. Атомарність

Робіть задачі маленькими. Погано: Одна задача "Зробити звіт", яка працює 10 хвилин. Добре: Chain: "Зібрати дані" -> "Відформатувати" -> "Зберегти PDF". Якщо впаде "Зберегти PDF", вам не треба буде знову 10 хвилин збирати дані. Ви просто перезапустите останній крок.


6. 🧩 Підсумок

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

  • Chain: Коли порядок має значення ($A \to B \to C$).
  • Group: Коли важлива швидкість і масовість ($A, B, C$ разом).
  • Chord: Коли треба зібрати результати масової роботи ($[A, B, C] \to D$).

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

Що далі? Уявіть, що одна ланка ланцюга зламалася. Що робити? Зупинити все? Спробувати ще раз? Проігнорувати? На наступному уроці ми поговоримо про Error Handling та Retries — як зробити вашу систему куленепробивною.

А поки — кодіть, експериментуйте і пам'ятайте: This is CS50! (тобто, Celery). 😉