Модуль 7

Delay, apply_async та параметри виконання

Ось урок, згенерований у стилі David Malan (CS50). Уявіть, що ми знаходимося у величезній аудиторії Сандерс-театру в Гарварді, я ходжу сценою, активно жестикулюю, а за моєю спиною — величезний екран із кодом.


🎓 Урок: Delay, apply_async та мистецтво відкладеного виконання

Привіт, світе! Радий вас бачити. 👋

Сьогодні ми зазирнемо під капот наших асинхронних задач. Ми вже знаємо, що таке Celery і навіщо нам "воркери". Але як саме ми передаємо їм команди? Як сказати: "Зроби це негайно!", а як: "Зроби це через 10 хвилин, і якщо не вийде — спробуй ще раз"?

Саме тут на сцену виходять два наші сьогоднішні герої: delay() та apply_async().


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

Уявіть, що ви прийшли у вишукану кав’ярню. Ви замовляєте еспресо. Є два варіанти розвитку подій:

  1. Синхронний жах: Бариста приймає замовлення, повертається до кавомашини, меле зерна, варить каву, наливає її... А ви стоїте і чекаєте. І вся черга за вами — теж чекає. Ніхто не може зробити замовлення, поки ви не отримаєте свою чашку. Це блокування. Це повільно.
  2. Асинхронна магія: Бариста приймає замовлення, кричить на кухню "Одне еспресо!", дає вам пейджер і одразу приймає наступного клієнта. Кухня (воркер) готує каву паралельно.

Ми вже знаємо, як це робити в коді, правда? Ми просто відправляємо задачу.

Але ось питання: А що, якщо ви хочете, щоб вам принесли каву не зараз, а рівно через 15 хвилин, до вашого десерту? Або що робити, якщо кавомашина зламалася? Чи варто пробувати зварити каву знову через хвилину, чи просто скасувати замовлення?

Простий виклик задачі не дає нам такої гнучкості. Нам потрібен пульт керування часом і умовами. Нам потрібен контроль.

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


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

Давайте розберемося. У Celery (найпопулярнішій бібліотеці для цього в Python) є два основні способи викликати задачу.

🔹 Метод delay()

Це "швидка кнопка". Це синтаксичний цукор. Коли ви пишете task.delay(arg), Celery насправді каже: "Я беру ці аргументи і відправляю задачу в чергу просто зараз, зі стандартними налаштуваннями".

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

🔹 Метод apply_async()

Це "панель керування польотами". Насправді, delay() — це просто скорочена версія apply_async(). apply_async дозволяє вам передати не тільки аргументи для функції (args, kwargs), але й параметри виконання (execution options).

Ось головні параметри, які треба знати (обов'язково!):

  1. countdown (зворотний відлік): "Виконай це не раніше, ніж через N секунд".
  2. eta (Estimated Time of Arrival): "Виконай це в конкретну дату і час".
  3. expires (термін придатності): "Якщо задача не виконалась до цього часу, забудь про неї, вона вже не актуальна".
  4. retry: "Якщо сталася помилка, спробуй ще раз".

Інтуїтивне розуміння: delay(...) — це як відправити SMS: "Передзвони мені". apply_async(...) — це як створити подію в календарі: "Зідзвон завтра о 14:00, нагадати за 10 хв, якщо не відповім — повторити через годину".


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

Уявімо, що у нас є функція, яка вітає користувача після реєстрації.

@app.task
def send_welcome_email(username):
    print(f"📧 Відправляю листа для {username}...")

Приклад 1: Класичний delay

# Найпростіший варіант
send_welcome_email.delay("Oleg")

Питання до вас: Коли користувач отримає листа? Відповідь: Майже миттєво (якщо черга вільна). Воркер підхопить задачу одразу.


Приклад 2: Використання countdown (Затримка)

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

# Використовуємо apply_async
# args — це список аргументів функції
send_welcome_email.apply_async(args=["Oleg"], countdown=600)

Що тут відбувається? Задача потрапляє до брокера (Redis/RabbitMQ), але воркер не чіпає її 600 секунд. Вона "лежить на полиці".


Приклад 3: expires (Актуальність)

Реальний кейс: Код підтвердження входу (2FA). Він дійсний лише 5 хвилин. Якщо через навантаження на сервери воркер дістався до задачі через 10 хвилин — відправляти SMS вже немає сенсу, код прострочений.

# Якщо задача не виконається протягом 300 секунд — видалити її
send_sms_code.apply_async(args=["+38050..."], expires=300)

Питання: Що буде, якщо воркер забере задачу через 301 секунду? Відповідь: Celery побачить, що час сплив, і просто викине цю задачу (Revoked). Ресурси не витрачено! Елегантність! 🎩


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

А тепер ваша черга! Відкривайте ваші IDE (або уявіть, що вони відкриті).

Завдання 1: "Нагадування про воду" 💧

Напишіть виклик задачі remind_drink_water, яка надішле повідомлення користувачу Alice рівно через 1 годину. (Підказка: 1 година = скільки секунд?)

Завдання 2: "Новорічне привітання" 🎄

Використовуючи модуль datetime, сформуйте виклик send_new_year_greeting, який спрацює точно 1 січня о 00:00. (Підказка: тут краще підійде eta чи countdown?)

Завдання 3: "Виправте помилку" 🐛

Студент написав такий код для відправки звіту, який потрібен "сьогодні або ніколи":

# Помилка тут!
generate_report.delay(user_id=42, expires=60)

Чому це не спрацює? Як це переписати правильно, використовуючи apply_async?

Завдання 4: Міні-кейс "Покинутий кошик" 🛒

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


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

Послухайте уважно. Зараз я розповім те, що відрізняє новачка від Senior-розробника.

⛔ Типова помилка #1: eta та countdown — це не гарантія!

Коли ви ставите countdown=60, ви кажете: "Не виконуй це раніше, ніж через 60 секунд". Ви НЕ кажете: "Виконай це рівно на 60-й секунді". Якщо всі ваші воркери зайняті обробкою інших задач, ваша задача чекатиме. Час у apply_async — це мінімальний поріг.

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

Ніколи, чуєте, НІКОЛИ не передавайте в args цілі об'єкти бази даних (наприклад, об'єкт User з Django). Чому? 1. Поки задача чекає в черзі (наприклад, годину), користувач може змінити email в базі. А у вашому об'єкті в задачі — старі дані! 2. Об'єкти важко серіалізувати (перетворити в текст для черги).

✅ Як думає профі: "Я передам тільки user_id. А коли воркер почне виконувати задачу, він сам піде в базу і дістане найсвіжіші дані про цього користувача."

# ❌ ПОГАНО:
send_email.apply_async(args=[user_object])

# ✅ ДОБРЕ:
send_email.apply_async(args=[user_object.id])

6. 🧩 Підсумок

Отже, що ми сьогодні вивчили?

  1. delay() — це для простих задач: "Вогонь!", і побігли.
  2. apply_async() — це швейцарський ніж. Тут ми керуємо часом (countdown, eta) і життям задачі (expires).
  3. Ми зрозуміли, що асинхронність — це не тільки про "паралельно", це про планування.

Тепер ви можете не просто виконувати код, а будувати складні часові сценарії взаємодії з користувачем. Ви керуєте часом у вашому додатку.

Що далі? Уявіть, що вам треба завантажити картинку, потім змінити її розмір, потім накласти водяний знак, і тільки потім — відправити на S3. І якщо один крок впаде — зупинити все. Звучить як ланцюжок? На наступному уроці ми поговоримо про Celery Canvas: Chains, Groups та Chords. Це буде справжня магія оркестрації!

А поки що — це був CS50... тобто, наш урок. До зустрічі! 💻🚀