Ось повноцінний урок у стилі 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). 😉