فصل ۷: API و امکانات حرفه‌ای — DRF، کش، Celery و امنیت

کارهای پس‌زمینه با Celery و Redis؛ ارسال ایمیل

کاربر نباید منتظر سرویس پیامک بماند

ارسال پیامک، ایمیل فاکتور، ساخت PDF، ورود اکسل هزارخطی: هر کاری که بیش از چند صد میلی‌ثانیه طول می‌کشد یا به سرویس بیرونی وابسته است، نباید در چرخه‌ی درخواست انجام شود. اگر سرویس پیامک ده ثانیه جواب ندهد، worker gunicorn ده ثانیه قفل است. Celery این کارها را در صف (Redis) می‌گذارد و پروسه‌های جداگانه (worker) آن‌ها را اجرا می‌کنند.

pip install "celery[redis]"
# config/celery.py
import os
from celery import Celery

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
app = Celery("config")
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()

# config/__init__.py
from .celery import app as celery_app
__all__ = ("celery_app",)

# settings.py
CELERY_BROKER_URL = os.getenv("REDIS_URL", "redis://127.0.0.1:6379/0")
CELERY_TIMEZONE = TIME_ZONE
CELERY_TASK_ACKS_LATE = True
CELERY_TASK_TIME_LIMIT = 120

تعریف task

# apps/notifications/tasks.py
import requests
from celery import shared_task
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string

from apps.orders.models import Order


@shared_task(bind=True, autoretry_for=(requests.RequestException,),
             retry_backoff=True, retry_kwargs={"max_retries": 5})
def send_order_sms(self, order_id):
    order = Order.objects.select_related("customer__user").get(pk=order_id)
    sms_client.send_pattern(order.customer.user.mobile, "order-created",
                            {"code": order.tracking_code})


@shared_task
def send_invoice_email(order_id):
    order = Order.objects.get(pk=order_id)
    ctx = {"order": order}
    msg = EmailMultiAlternatives(
        subject=f"فاکتور سفارش {order.tracking_code}",
        body=render_to_string("emails/invoice.txt", ctx),
        to=[order.customer.user.email],
    )
    msg.attach_alternative(render_to_string("emails/invoice.html", ctx), "text/html")
    msg.send()

فراخوانی: transaction.on_commit(lambda: send_order_sms.delay(order.pk)). اجرای worker و زمان‌بند:

celery -A config worker -l info
celery -A config beat -l info          # کارهای دوره‌ای (CELERY_BEAT_SCHEDULE)
# روی ویندوز برای توسعه:
celery -A config worker -l info --pool=solo

تنظیمات ایمیل

EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "mail.example.ir"
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = os.getenv("EMAIL_USER")
EMAIL_HOST_PASSWORD = os.getenv("EMAIL_PASSWORD")
DEFAULT_FROM_EMAIL = "فرش کاشان <noreply@example.ir>"
# در توسعه: "django.core.mail.backends.console.EmailBackend"

نکته‌هایی که کمتر کسی می‌داند

  • به task همیشه شناسه بدهید نه شیء مدل؛ شیء سریال‌سازی می‌شود و وقتی worker آن را اجرا می‌کند ممکن است داده‌اش کهنه باشد.
  • بدون on_commit، worker ممکن است قبل از commit تراکنش task را بگیرد و با DoesNotExist روبه‌رو شود؛ باگی که فقط زیر بار واقعی دیده می‌شود. از Celery 5.4 می‌توانید مستقیم task.delay_on_commit(...) را صدا بزنید.
  • Celery 4 به بعد روی ویندوز رسماً پشتیبانی نمی‌شود؛ pool پیش‌فرض prefork آن‌جا کار نمی‌کند و --pool=solo فقط برای توسعه است.
  • با acks_late اگر worker وسط کار از کار بیفتد، task دوباره اجرا می‌شود؛ پس taskها را idempotent بنویسید (اجرای دوباره نباید پیامک دوم بفرستد؛ مثلاً پرچم sms_sent).
  • برای پروژه‌های کوچک که Redis و Celery سربار زیادی است، Django 6.0 چارچوب داخلی Tasks را آورده؛ روی 5.x، کتابخانه‌هایی مثل django-q2 یا huey با پیکربندی کمتر همین کار را می‌کنند.

برای ذخیره‌ی پیشرفت و شرکت در آزمون، وارد شوید — رایگان است.