کاربر نباید منتظر سرویس پیامک بماند
ارسال پیامک، ایمیل فاکتور، ساخت 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 با پیکربندی کمتر همین کار را میکنند.