Модуль 24

Celery у Django-проєкті

Ось урок, створений за твоїм майстер-промптом. Вмикаємо режим CS50! 🚀


🎓 CS50 Style: Celery у Django. Асинхронність та черги задач

Привіт, друзі! Радий бачити вас. Сьогодні ми поговоримо про те, що відрізняє "гальмуючий" сайт від того, який літає як ракета.


1. 🔥 Вступ: Чому ваш сайт "зависає"?

Уявіть ситуацію. Ви заходите на сайт, реєструєтесь, натискаєте кнопку "Створити акаунт" і... чекаєте. Крутиться колесо завантаження. Секунда... дві... п'ять... десять.

Ви починаєте нервувати. "Чи завис інтернет? Чи я натиснув кнопку двічі?". Зрештою, сторінка оновлюється: "Вітаємо, лист із підтвердженням надіслано!".

Питання до вас: Чому ви чекали 10 секунд? Хіба запис користувача в базу даних займає стільки часу? Ні, це мілісекунди. Ви чекали, поки поштовий сервер (SMTP) з’єднається, відправить лист і скаже вашому серверу "Ок".

У чому проблема?

Django за замовчуванням — синхронний. Це як черга в касу супермаркету. Якщо покупець перед вами вирішив оплатити дріб’язком і рахує копійки (відправляє "важкий" email або обробляє відео), ви не можете пройти далі, доки він не закінчить.

Касир (Django) заблокований. Всі чекають. Це поганий UX (User Experience).

Аналогія: Ресторан 🍽️

Уявіть ресторан. * Django — це офіціант. * Ви — клієнт. * Важка задача (згенерувати звіт / відправити email) — це приготування стейка.

Якщо офіціант прийме ваше замовлення і сам піде на кухню смажити м'ясо, поки ви чекаєте, — ресторан збанкрутує. Інші клієнти не зможуть навіть попросити меню!

Правильний підхід: Офіціант бере замовлення, вішає папірець на кухню і миттєво повертається до вас: "Замовлення прийнято, скоро буде". А на кухні (у "фоні") кухарі вже працюють.

Ось ці "кухарі" — це і є Celery.


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

Отже, нам потрібно винести важкі задачі за межі циклу "Запит –> Відповідь" (Request/Response Cycle).

У нас з'являються три головні гравці. Запам’ятайте цю трійцю:

  1. Producer (Виробник) — Django: Він створює задачу. Але замість того, щоб виконувати її, він каже: "Гей, зробіть це, коли буде час!".

  2. Broker (Посередник) — Redis або RabbitMQ: Це дошка оголошень або кошик для паперів на кухні. Django кладе туди повідомлення: "Задача №1: Відправити лист юзеру ID=5". Django не знає, хто і коли це зробить. Його робота — покласти повідомлення і піти далі.

  3. Consumer (Споживач) — Celery Worker: Це власне "кухар". Окремий процес, який постійно дивиться на Брокера. "О, нове повідомлення в Redis! Беру в роботу!".

💡 Що треба зрозуміти інтуїтивно:

Celery — це просто спосіб запустити Python-код в іншому процесі, паралельно з вашим сайтом.

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

Django і Celery Worker — це різні процеси. Вони можуть навіть жити на різних серверах. Вони спілкуються тільки через Брокера (Redis). Вони не мають спільної оперативної пам'яті!


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

Крок 0: Налаштування (швидко)

В реальному житті ви встановите бібліотеки: pip install celery redis

І створите файл celery.py поруч із settings.py, щоб Django знав про Celery. Але давайте подивимось на логіку.

Приклад 1: "Hello World" (Мінімальний)

Звичайний Python (блокує):

import time

def add_slowly(x, y):
    time.sleep(5)  # Імітуємо важку роботу
    return x + y

# Django чекає 5 секунд тут:
result = add_slowly(4, 4)
print("Готово!")

З Celery (асинхронно):

from celery import shared_task
import time

# 1. Декоратор перетворює функцію на задачу Celery
@shared_task
def add_slowly(x, y):
    time.sleep(5)
    return x + y

# 2. Виклик через .delay()
# Django НЕ чекає! Він миттєво отримує ID задачі і йде далі.
task = add_slowly.delay(4, 4)

print(f"Задача відправлена! Її ID: {task.id}")

Питання до студента: Що буде, якщо я запущу цей код, але забуду запустити Celery Worker у терміналі? (Пауза для роздумів) Відповідь: Django відпрацює миттєво, скаже "Задача відправлена!", повідомлення впаде в Redis... і буде там лежати вічно. Ніхто його не забере, бо "кухарі" не вийшли на зміну.

Приклад 2: Реальний проєкт (Email)

Типова ситуація у views.py.

# views.py
from django.shortcuts import render
from .tasks import send_welcome_email_task

def register_user(request):
    # ... логіка збереження юзера ...
    user = form.save()

    # ЗАМІСТЬ того, щоб відправляти лист тут:
    # send_mail(...) <--- Це повільно!

    # Ми кажемо Celery: "Зроби це фоново"
    # Важливо: передаємо ID юзера, а не об'єкт user! (чому — розберемо далі)
    send_welcome_email_task.delay(user.id)

    return render(request, 'success.html')

І сам tasks.py:

# tasks.py
from celery import shared_task
from django.core.mail import send_mail
from django.contrib.auth.models import User

@shared_task
def send_welcome_email_task(user_id):
    # Worker сам дістає юзера з бази
    user = User.objects.get(id=user_id)

    send_mail(
        'Вітаємо!',
        f'Привіт, {user.username}!',
        'admin@mysite.com',
        [user.email],
    )
    return "Email sent"

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

Прийшов час забруднити руки кодом. Уявіть, що у вас є запущений Django-проєкт.

Завдання 1: Перший запуск Напишіть просту функцію multiply(a, b) у файлі tasks.py, позначте її @shared_task. Викличте її в Django shell (python manage.py shell) через .delay(5, 5). Що повернула консоль? (Має бути AsyncResult об'єкт, а не число 25).

Завдання 2: Зламайте це Зупиніть процес Celery Worker (Ctrl+C). Знову викличте задачу в shell. Подивіться в адмін-панель або логи. Чи видав Django помилку? Спойлер: Ні. Django свою роботу зробив — віддав задачу брокеру.

Завдання 3: Виправлення помилки У вас є задача, яка обробляє зображення: process_avatar(image_binary_data). Ви намагаєтесь передати саме зображення (байти) в .delay(). Celery падає з помилкою серіалізації (JSON error). Як це виправити? (Підказка: Передавайте шлях до файлу або ID моделі, де лежить картинка, а не сам файл).

Завдання 4: Міні-кейс "Щоденний звіт" Менеджер просить, щоб щоранку о 9:00 йому приходив звіт про продажі. Питання: Чи достатньо тут простого .delay()? Рішення: Ні, тут потрібен Celery Beat (планувальник), який штовхає задачі за розкладом. Спробуйте нагуглити, як додати періодичну задачу в CELERY_BEAT_SCHEDULE.


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

Ось тут відрізняється Junior від Senior. Слухайте уважно.

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

Ніколи не робіть так:

# ПОГАНО ❌
task.delay(user_object) 

Доки повідомлення лежить у черзі (Redis), дані в базі можуть змінитись! А ще об'єкт може бути складним для перетворення в JSON. Завжди передавайте первинні ключі (ID):

# ДОБРЕ ✅
task.delay(user_object.id)

Нехай Worker сам свіженьке дістане з бази.

⛔ Типова помилка №2: Race Condition (Стан гонитви)

Ви зберігаєте юзера і відразу викликаєте задачу.

user = User.objects.create(...)
task.delay(user.id)
# Транзакція в БД може ще не встигнути зафіксуватися (commit)!

Worker (який дуже швидкий) отримує ID, лізе в базу... а там такого ID ще немає, бо Django ще не завершив транзакцію. Boom! DoesNotExist. Рішення: Використовуйте transaction.on_commit(lambda: task.delay(user.id)).

Порада профі:

Думайте про Celery як про ненадійного працівника. Світло може зникнути. Задача може впасти. Ваші задачі мають бути ідемпотентними. Це означає: якщо виконати задачу двічі (випадково), нічого страшного не станеться (клієнт не отримає два листи, гроші не спишуться двічі).


6. 🧩 Підсумок

Ну що, підіб'ємо підсумки?

  1. Django — синхронний. Він не любить чекати.
  2. Celery — це фоновий працівник, який виконує важкі задачі асинхронно.
  3. Redis — це клей між ними (брокер повідомлень).
  4. Ми використовуємо .delay(), щоб віддати наказ і не чекати результату.
  5. Ми передаємо ID, а не цілі об'єкти.

Тепер ви вмієте: робити так, щоб ваш сайт відповідав користувачеві миттєво, навіть якщо під капотом відбувається важка магія обробки даних.

Наступного разу: Ми розберемося, як "загорнути" все це (Django + Celery + Redis) у Docker, щоб запускати однією командою, а не відкривати три різні термінали.

А поки що — це був CS50 (в душі). Кодіть із задоволенням! 👋