تنفيذ المهام الخلفية باستخدام Celery و Redis في Django

تنفيذ المهام الخلفية باستخدام Celery و Redis في Django

في المشاريع الصغيرة قد يبدو كل شيء بسيطًا: المستخدم يضغط زرًا، فتقوم الصفحة بإرسال بريد إلكتروني، أو توليد ملف PDF، أو مزامنة بيانات مع خدمة خارجية، ثم تعود الاستجابة بسرعة وكأن شيئًا لم يحدث. لكن عندما يكبر المشروع وتتزايد الطلبات، تبدأ المشكلة الحقيقية بالظهور. بعض العمليات لا ينبغي أن تُنفَّذ داخل نفس الطلب الذي ينتظر المستخدم نتيجته، لأن ذلك يبطّئ الصفحة، ويزيد احتمال الفشل، ويضع ضغطًا غير ضروري على الخادم. هنا تظهر قيمة المهام الخلفية، وهنا يتألق Celery مع Redis داخل Django. فبدلًا من أن ينتظر المستخدم انتهاء عملية ثقيلة، تُرسل المهمة إلى طابور، ويتكفّل عامل منفصل بتنفيذها في الخلفية، بينما يبقى التطبيق الرئيسي سريعًا ومتجاوبًا. Celery هو نظام طوابير مهام موزعة يركز على المعالجة اللحظية مع دعم الجدولة أيضًا، ويعمل عبر الرسائل بين العميل والعامل بواسطة وسيط يُسمّى broker، كما يمكنه استخدام backends مختلفة لتخزين النتائج. وRedis يمكن أن يعمل كـ broker وكـ result backend في نفس الوقت، وهو ما يجعله خيارًا شائعًا جدًا في تطبيقات Django الحديثة.

ما أجمل هذا النمط عندما يُستخدم بالشكل الصحيح: صفحة الدفع تنتهي بسرعة، ورسالة التأكيد تُرسل في الخلفية، وتحديث المخزون يتم بدون أن يشعر المستخدم بأي تأخير، وجدول المهام اليومية يعمل في الوقت المحدد، ومع ذلك يبقى التطبيق قادرًا على التوسع دون أن يتحول إلى كتلة من الطلبات المزدحمة. الفكرة هنا ليست مجرد “تشغيل كود في الخلفية”، بل بناء طبقة تشغيل مستقلة للمهام التي لا تحتاج إلى جواب فوري، أو تلك التي لا يصح أن تحجز الطلب الأساسي. ولهذا السبب تحديدًا أصبحت Celery واحدة من أكثر الأدوات حضورًا في منظومة Python، خصوصًا عندما يُدمَج مع Django بطريقة منظمة وواضحة.

لماذا نحتاج إلى المهام الخلفية أصلًا؟

تخيل أن لديك متجرًا إلكترونيًا، وعند إتمام الطلب تريد أن تفعل عدة أشياء: تحفظ الفاتورة، ترسل بريدًا للمستخدم، تخبر فريق المخزن، تحدّث لوحة الإحصائيات، وتدفع إشعارًا إلى خدمة خارجية عبر API. لو نفّذت كل ذلك داخل نفس request-response cycle، فقد يتأخر الرد لثوانٍ طويلة، وقد يفشل كله إذا تعطلت خدمة بريد واحدة. المستخدم في هذه اللحظة لا يهتم بأنك أنجزت خمس عمليات دفعة واحدة؛ كل ما يريده هو أن يرى أن الطلب تم بنجاح وبسرعة. لذلك فإن فصل الأعمال الثقيلة إلى مهام خلفية يعطيك ثلاث فوائد كبيرة: تسريع الاستجابة للمستخدم، عزل الأعطال الجزئية، وإتاحة التوسع الأفقي عندما يزيد الحمل. وهذا بالضبط ما صُممت له أنظمة الطوابير مثل Celery: أخذ العمل على شكل رسائل، وتمريره إلى عمال مستقلين، ثم تنفيذ العمل بعيدًا عن التطبيق الذي يستقبل الطلبات.

وهناك سبب آخر أكثر أهمية أحيانًا: ليس كل عمل يجب أن يُنفَّذ فورًا. بعض الأعمال يمكن تأجيلها ثواني أو دقائق أو ساعات، مثل إرسال نشرات البريد، توليد تقارير مجمعة، تنظيف البيانات القديمة، إعادة حساب الإحصائيات، أو مزامنة عمليات تتم على دفعات. عندما تُخطئ في هذا الفصل، يصبح كل طلب web request متضخمًا وكأنه مشروع مستقل، فتبدأ المشاكل في الظهور: timeouts، وازدحام في قاعدة البيانات، وتجربة مستخدم بطيئة، وصعوبة في مراقبة الأخطاء. أما عندما تصوغ العمل على هيئة tasks، فإنك تمنح النظام القدرة على التنفس.

ما هي Celery وما هو Redis في هذه المعادلة؟

Celery، بحسب وثائقها الرسمية، هي task queue موجهة للمعالجة اللحظية مع دعم الجدولة، وتستعمل نموذج الرسائل بين العميل والعامل عبر broker. يمكن أن تتكون المنظومة من أكثر من worker وأكثر من broker، ما يسمح بالتوسع والمرونة. أما Redis، ففي وثائقه الرسمية يوصف بأنه مخزن بيانات في الذاكرة يُستخدم أيضًا كـ cache وmessage broker وstreaming engine، ويدعم أنواعًا متعددة من البيانات وعمليات ذرّية عليها. وهذا ما يفسر لماذا يعمل Redis مع Celery بهذه السلاسة: فهو سريع جدًا في تمرير الرسائل، ويمكن استخدامه كذلك لتخزين نتائج المهام عند الحاجة.

في Celery يوجد مفهوم مهم يجب أن تفهمه من البداية: broker ليس هو مكان تخزين البيانات النهائي، بل هو الوسيط الذي ينقل الرسالة من التطبيق إلى العامل. وعند إرسال task، لا “تنفَّذ” المهمة داخل نفس العملية التي استقبلت الطلب، بل تُوضَع رسالة في الطابور. العامل worker يراقب الطابور باستمرار، وعندما يجد رسالة جديدة يأخذها وينفذ الكود المرتبط بها. وإذا أردت تخزين نتيجة التنفيذ أو حالة المهمة، يمكنك استخدام result backend، والذي يمكن أن يكون Redis أو قاعدة بيانات أو غير ذلك بحسب الإعداد. Celery توضح رسميًا أن result_backend لا يكون مفعّلًا افتراضيًا، وأن Redis من الخيارات المدعومة لتخزين النتائج. كما توضح وثائقها أن Redis يمكن أن يكون broker وbackend في الوقت نفسه، مع التنبيه إلى أن الرسائل الكبيرة قد تسبّب ازدحامًا إذا أسيء استخدامها.

هذا التفريق مهم جدًا، لأنه يغيّر طريقة تفكيرك بالكامل. عندما تكون المهمة قصيرة جدًا وغير مكلفة، ربما لا تحتاج Celery أصلًا. لكن عندما تصبح المهمة طويلة أو قابلة للفشل أو تحتاج إعادة المحاولة أو الجدولة أو التوزيع، هنا يبدأ Celery في تقديم قيمته الحقيقية. وإذا كانت المهمة من النوع الذي يحتاج إلى الالتزام بالوقت أو التكرار، فإن Celery beat يضيف طبقة جدولة تُطلق المهام دوريًا، وهو في الوثائق الرسمية موصوف كـ scheduler يطلق المهام وفق فترات منتظمة، مع إمكانية أخذ الإعدادات من beat_schedule أو من مخزن مخصص مثل قاعدة بيانات.

متى تستخدم Celery مع Redis في Django؟

يمكنك استخدام هذا الثنائي في حالات كثيرة جدًا، لكن أشهرها: إرسال البريد الإلكتروني، توليد الملفات الثقيلة، معالجة الصور والفيديو، جلب بيانات من API خارجي، تحديث مؤشرات وتقارير دورية، إشعار الخدمات الأخرى عند حصول حدث معين، وتنفيذ عمليات خلفية متسلسلة أو متوازية. من الجميل في Django أن بنية المشروع تسمح بفصل الوظائف بسهولة داخل ملفات tasks.py، وهذا ما توصي به وثائق Celery الخاصة بـ Django، حيث تشير إلى تعريف المهام في وحدة منفصلة مثل tasks.py وربط Celery بالمشروع Django عبر إعدادات واضحة.

لكن ليس كل شيء مناسبًا للمهام الخلفية. إذا كانت العملية تحتاج إلى نتيجة فورية في نفس الرد، أو إذا كانت خفيفة جدًا بحيث لا تبرر إرسال رسالة إلى broker، فقد يكون التنفيذ المباشر أفضل. المهم أن يكون القرار مبنيًا على طبيعة العمل لا على الحماس للتقنية. أحيانًا يكون مجرد save() في Django أو @transaction.atomic مع منطق واضح أفضل وأبسط من إدخال Celery في مكان لا يحتاجه. وعندما تكون هناك عملية تحتاج فقط إلى تنسيق التنفيذ داخل قاعدة البيانات نفسها، فإن الحفاظ على البساطة قد يكون أكثر احترافية من إضافة بنية تحتية إضافية.

تجهيز المشروع من الصفر

لنبدأ بمشروع Django افتراضي اسمه myproject وتطبيق اسمه core. ستحتاج أولًا إلى تثبيت الحزم الأساسية:

pip install django celery redis

إذا كنت تريد تشغيل Redis محليًا عبر Docker أثناء التطوير:

docker run -p 6379:6379 redis:7

بعد ذلك ستحتاج إلى تنظيم ملفات المشروع بحيث يضم Celery إعداداته الخاصة. البنية الشائعة تكون على هذا الشكل:

myproject/
│
├── manage.py
├── myproject/
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   ├── asgi.py
│   ├── wsgi.py
│   └── celery.py
│
└── core/
    ├── __init__.py
    ├── views.py
    ├── tasks.py
    ├── models.py
    └── admin.py

ملف myproject/celery.py

هذا الملف هو قلب الربط بين Django وCelery:

import os
from celery import Celery

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")

app = Celery("myproject")

app.config_from_object("django.conf:settings", namespace="CELERY")

app.autodiscover_tasks()

الجزء المهم هنا هو config_from_object مع namespace="CELERY"، لأنك بذلك تستطيع كتابة إعدادات Celery داخل settings.py بأسماء تبدأ بـ CELERY_ بشكل منظم، كما أن autodiscover_tasks() يجعل Celery يبحث تلقائيًا عن ملفات tasks.py داخل التطبيقات المثبتة. هذا النمط متوافق مع أسلوب Django المعتاد في تنظيم الملفات، وهو أيضًا قريب من التوصيات الرسمية الخاصة بدمج Celery مع Django.

ملف myproject/__init__.py

حتى يتعرّف Django على تطبيق Celery عند بدء التشغيل، أضف:

from .celery import app as celery_app

__all__ = ("celery_app",)

هذا السطر الصغير يبدو بسيطًا جدًا، لكنه مهم للغاية لأنه يضمن تحميل تطبيق Celery عند استيراد الحزمة الرئيسية للمشروع. في مشاريع كثيرة، يحدث الخطأ الشهير: “المهمة لا تُكتشف” فقط لأن هذا الربط البسيط لم يُضَف في المكان الصحيح.

إعدادات Redis وCelery داخل settings.py

CELERY_BROKER_URL = "redis://127.0.0.1:6379/0"
CELERY_RESULT_BACKEND = "redis://127.0.0.1:6379/1"

CELERY_ACCEPT_CONTENT = ["json"]
CELERY_TASK_SERIALIZER = "json"
CELERY_RESULT_SERIALIZER = "json"

CELERY_TIMEZONE = "Africa/Casablanca"
CELERY_ENABLE_UTC = True

في هذا المثال استخدمنا قاعدة Redis مختلفة للـ broker وأخرى للـ result backend فقط للتنظيم. هذا ليس إلزاميًا، لكنه أسلوب جيد أثناء التطوير. وبما أن Celery يدعم Redis backend لتخزين النتائج، فهذه الإعدادات منطقية ومباشرة. كما أن ضبط المنطقة الزمنية مهم جدًا عندما تبدأ في المهام الدورية أو المهام المؤقتة، لأن الجدولة الخاطئة قد تجعل المهمة تعمل بوقت غير متوقع. Celery beat هو scheduler يطلق المهام دوريًا، لذا من المهم أن يكون الوقت مضبوطًا من البداية.

أول مهمة حقيقية: إرسال بريد إلكتروني في الخلفية

لنكتب مهمة بسيطة داخل core/tasks.py:

from celery import shared_task
from django.core.mail import send_mail

@shared_task
def send_welcome_email(user_email, user_name):
    subject = "مرحبًا بك معنا"
    message = f"أهلًا {user_name}، سعدنا بانضمامك إلى منصتنا."
    from_email = "noreply@example.com"

    send_mail(subject, message, from_email, [user_email])
    return f"Email sent to {user_email}"

استخدمنا @shared_task بدلًا من ربط المهمة مباشرة بتطبيق Celery، وهذا يمنحك مرونة أكبر داخل Django apps. عند استدعاء هذه المهمة، لن يُرسَل البريد داخل نفس الطلب، بل ستُسجَّل المهمة وتُنفَّذ بواسطة worker.

ثم من أي view في Django يمكنك تشغيلها هكذا:

from django.http import JsonResponse
from core.tasks import send_welcome_email

def register_user(request):
    email = request.GET.get("email", "user@example.com")
    name = request.GET.get("name", "Ahmed")

    send_welcome_email.delay(email, name)

    return JsonResponse({
        "status": "success",
        "message": "تم تسجيلك بنجاح، وسيصلك البريد خلال لحظات."
    })

هنا يظهر جمال Celery بوضوح: الرد يرجع بسرعة، والمهمة تُنفَّذ في الخلفية. delay() هي من الطرق الشائعة لاستدعاء المهام، كما أن Celery توفر API موحدًا للاستدعاء يشمل delay() وapply_async() وغيرهما.

delay() أم apply_async()؟

في الحالات البسيطة delay() تكفي، لكنها ليست الوحيدة. عندما تحتاج إلى تحكم أكبر، استخدم apply_async():

send_welcome_email.apply_async(
    args=["user@example.com", "Ahmed"],
    countdown=30
)

بهذا المثال نؤخر التنفيذ 30 ثانية. ويمكنك أيضًا تحديد eta لتشغيل المهمة عند وقت محدد. وثائق Celery الخاصة بالاستدعاء توضّح أن هناك API موحدًا للـ calling methods، وأنه يمكنك تمرير خيارات التنفيذ بشكل صريح. هذا الفرق بسيط من الخارج، لكنه مهم جدًا عندما تبدأ في بناء سلوكيات معقدة: تأخير، جدولة دقيقة، إعادة محاولة لاحقة، أو ربط المهام ببعضها.

معالجة الصور أو الملفات الثقيلة

من أكثر الاستخدامات شيوعًا في Django مع Celery: معالجة الصور بعد رفعها. مثلًا، إذا رفع المستخدم صورة شخصية، يمكنك حفظ الملف فورًا ثم إطلاق مهمة تقوم بإنشاء نسخة مصغرة، أو ضغط الصورة، أو رفعها إلى تخزين خارجي، أو استخراج معلومات منها.

from celery import shared_task
from PIL import Image
from pathlib import Path

@shared_task
def create_thumbnail(image_path):
    original = Path(image_path)
    thumb_path = original.with_name(f"thumb_{original.name}")

    img = Image.open(original)
    img.thumbnail((300, 300))
    img.save(thumb_path)

    return str(thumb_path)

وفي الـ view:

from django.http import JsonResponse
from core.tasks import create_thumbnail

def upload_image(request):
    image_path = "/tmp/uploads/avatar.jpg"
    create_thumbnail.delay(image_path)

    return JsonResponse({
        "status": "queued",
        "message": "تم رفع الصورة وسيتم تجهيز النسخة المصغرة في الخلفية."
    })

الفكرة هنا ليست فقط “تشغيله في الخلفية”، بل أيضًا فصل الجزء الذي يحتاج I/O ثقيلًا عن الجزء الذي يتعامل مباشرة مع الطلب. وهذا الفصل يجعل تطبيقك أكثر ثباتًا عند الازدحام.

إعادة المحاولة عند الفشل: لأن العالم الحقيقي لا يرحم

الأعمال الخلفية لا تعني أن كل شيء سينجح من أول مرة. خدمات البريد قد تتعطل، والـ API الخارجي قد يرفض الطلب، والشبكة قد تنقطع، أو قاعدة البيانات قد تكون محمّلة. لذلك من أفضل ما يقدمه Celery هو إعادة المحاولة. يمكنك كتابة مهمة تعيد المحاولة تلقائيًا عند فشلها:

from celery import shared_task
import requests

@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def sync_user_profile(self, user_id):
    try:
        response = requests.get(f"https://api.example.com/users/{user_id}", timeout=10)
        response.raise_for_status()
        return response.json()
    except Exception as exc:
        raise self.retry(exc=exc)

في هذا المثال، إذا فشل الطلب الخارجي، ستُعاد المحاولة حتى ثلاث مرات، مع تأخير افتراضي 60 ثانية بين المحاولات. هذا النمط مهم جدًا في أي نظام يعتمد على الخدمات الخارجية. بدلًا من أن تسقط المهمة نهائيًا من أول glitch صغير، تمنحها فرصة ثانية وثالثة قبل أن تعلن الفشل.

لكن انتبه: إعادة المحاولة ليست عصا سحرية. إذا كانت المهمة نفسها غير قابلة للتكرار بأمان، فقد تكرر أثرًا جانبيًا لا تريد تكراره. لذلك يجب أن تكون المهمة idempotent كلما أمكن، أي أن إعادة تنفيذها لا تُفسد البيانات ولا تضاعف النتائج. هذه نقطة تصميمية لا يقل أهميتها عن الكود نفسه.

تنظيم المهام داخل التطبيق

في المشاريع التي تنمو بسرعة، لا يكفي أن تكتب task هنا وأخرى هناك بطريقة عشوائية. الأفضل أن تفصل المهام حسب المجال. مثلًا:

core/
├── tasks/
│   ├── __init__.py
│   ├── emails.py
│   ├── reports.py
│   ├── notifications.py
│   └── sync.py

ثم تعرّف المهام في كل ملف حسب نوعها. هذا يجعل الصيانة أسهل بكثير، خصوصًا إذا كان فريقك كبيرًا أو المشروع يعيش سنوات طويلة. ويمكنك كذلك استعمال Celery canvas عندما تبدأ بربط المهام: group لتجميع عدد من المهام، chain لتنفيذها بالتتابع، وchord لتنفيذ callbacks على مجموعات من المهام. وثائق Celery الرسمية تعرض هذه الأدوات ضمن API الأساسي الخاص بالتوزيع والمعالجة.

مثال سريع على chain:

from celery import chain
from core.tasks import task_a, task_b, task_c

workflow = chain(task_a.s(), task_b.s(), task_c.s())
workflow.delay()

ومثال على group:

from celery import group
from core.tasks import resize_image, watermark_image, optimize_image

job = group(
    resize_image.s("image.jpg"),
    watermark_image.s("image.jpg"),
    optimize_image.s("image.jpg"),
)
job.delay()

هذه الأدوات مفيدة جدًا عندما يكون العمل مركّبًا من خطوات، أو عندما تريد تنفيذ عدة مهام متشابهة بالتوازي ثم جمع النتائج بعد ذلك.

المهام الدورية باستخدام Celery Beat

بعض الأعمال يجب ألا تنتظر حدثًا من المستخدم أصلًا. مثلًا، تنظيف الجلسات القديمة، إرسال تقارير يومية، مزامنة بيانات كل ساعة، أو إعادة حساب ملخصات الإحصائيات في منتصف الليل. هنا يأتي دور Celery beat، الذي تصفه الوثائق الرسمية بأنه scheduler يطلق المهام على فترات منتظمة، مع إمكانية الاعتماد على beat_schedule أو مخزن مخصص.

إعداد مهمة دورية

في core/tasks.py:

from celery import shared_task
from django.utils import timezone

@shared_task
def daily_summary():
    today = timezone.now().date()
    return f"Daily summary generated for {today}"

وفي settings.py:

from datetime import timedelta

CELERY_BEAT_SCHEDULE = {
    "daily-summary-at-1am": {
        "task": "core.tasks.daily_summary",
        "schedule": 86400.0,
    },
}

يمكنك لاحقًا استبدال قيمة schedule بتوقيت أدق باستخدام crontab حسب احتياجك. المهم هنا أن تدرك أن beat لا ينفذ المهمة بنفسه، بل يرسلها إلى workers، تمامًا كما تفعل بقية الطوابير. لذلك فإن البنية المنطقية تظل نفسها: scheduler يقرر الوقت، والworker ينجز الفعل.

Redis: لماذا هو خيار ممتاز هنا؟

Redis يتميز بسرعة عالية جدًا لأنه يعمل أساسًا في الذاكرة، كما تشير وثائقه إلى أنه data store يُستخدم كـ cache وmessage broker وstreaming engine. هذا يجعله مناسبًا جدًا للرسائل القصيرة والمهام الخفيفة والمتوسطة. وثائق Celery نفسها تنبه إلى أن Redis يعمل جيدًا كـ broker للرسائل الصغيرة والسريعة، لكنها تحذر من أن الرسائل الكبيرة قد تسبب ازدحامًا. وهذه ملاحظة عملية جديرة بالاهتمام، لأن بعض المطورين يضعون بيانات ضخمة داخل task payload ثم يتساءلون لاحقًا لماذا أصبح النظام بطيئًا.

ومع ذلك، يجب أن تتعامل مع Redis بوعي. وثائق Redis توضّح أن عند استخدامه مع persistence يمكن أن يعتمد على RDB snapshots أو AOF logging، وأن عدم تفعيل persistence يعني أن البيانات قد لا تُحفظ على القرص. هذا يعني أن Redis رائع كطبقة سريعة جدًا، لكنه ليس بديلًا تلقائيًا عن التفكير في الاعتمادية. عندما تكون المهمة حرجة، لا تفترض أن كل شيء سيبقى محفوظًا إلى الأبد لمجرد أنك تستخدم Redis. افهم إعدادات الحفظ، وراقب الفشل، وحدد ما إذا كانت خسارة بعض الرسائل مقبولة أو لا.

كيف تصمم المهام بطريقة صحيحة؟

القاعدة الذهبية الأولى: اجعل المهمة صغيرة ومحددة. لا تجعل task واحدة تفعل كل شيء.
القاعدة الثانية: مرّر أقل قدر ممكن من البيانات. بدل أن تمرر كائنًا ضخمًا، مرر المعرف id واترك العامل يجلب ما يحتاجه.
القاعدة الثالثة: تأكد أن المهمة قابلة لإعادة التشغيل.
القاعدة الرابعة: افصل بين من يقرر العمل ومن ينفذه.
القاعدة الخامسة: راقب مدة التنفيذ، وعدد المحاولات، والأخطاء المتكررة.

مثال سيئ:

process_everything.delay(big_json_payload)

مثال أفضل:

process_everything.delay(order_id)

ثم داخل المهمة:

@shared_task
def process_everything(order_id):
    from core.models import Order
    order = Order.objects.get(id=order_id)
    # جلب البيانات اللازمة فقط
    # تنفيذ الخطوات المطلوبة

هذا الأسلوب أبسط للتتبع، وأخف على الشبكة، وأقوى عند الصيانة. كما أنه ينسجم أكثر مع فلسفة Celery التي تعتمد على الرسائل الصغيرة بين العميل والعامل.

التعامل مع قاعدة البيانات داخل المهام

عندما تعمل مهمة خلفية مع Django ORM، تذكّر أن worker عملية مستقلة. هذا يعني أن أي معاملة transaction لم تُحفظ بعد داخل request قد لا تكون مرئية للمهمة إذا أطلقتها مبكرًا. لذلك، في العمليات الحساسة، من الأفضل تشغيل المهمة بعد نجاح الحفظ النهائي، لا قبل ذلك. وإذا كان لديك تسلسل يعتمد على بيانات لم تُكتب بعد، فقد تجد المهمة تقرأ حالة غير مكتملة. هنا لا تكون المشكلة في Celery، بل في توقيت الإطلاق.

مثال عملي:

from django.db import transaction
from core.tasks import send_welcome_email

def create_customer(request):
    customer = Customer.objects.create(...)
    transaction.on_commit(lambda: send_welcome_email.delay(customer.email, customer.name))

استخدام on_commit فكرة ممتازة عندما تريد التأكد أن المهمة تُطلق فقط بعد اكتمال المعاملة. هذه من التفاصيل الصغيرة التي تصنع فرقًا كبيرًا في المشاريع الواقعية.

ملف إعدادات إنتاجي أكثر نضجًا

عندما تنتقل من التطوير إلى الإنتاج، ستحتاج إلى التفكير في تفاصيل أكثر من مجرد تشغيل celery -A myproject worker. على سبيل المثال، قد تضبط عدد الـ workers، ونوع concurrency، وأسماء queues المختلفة، وتفصل بين queue للمهام السريعة وأخرى للمهام الثقيلة. يمكنك أيضًا استخدام عدة workers على نفس الجهاز أو على أجهزة مختلفة حسب الحاجة. وثائق Celery تذكر أن النظام يمكن أن يعمل عبر عدة workers وبنية موزعة، وهو ما يسمح بالتوسع الأفقي.

مثال تشغيلي:

celery -A myproject worker -l info
celery -A myproject beat -l info

وفي بيئات الإنتاج غالبًا ستستعمل Supervisor أو systemd أو Docker Compose أو Kubernetes لإبقاء الخدمات حيّة وإعادة تشغيلها عند الحاجة. المهم أن تتعامل مع Celery كخدمة مستقلة، لا كقطعة جانبية داخل Django فقط. فالتطبيق الرئيسي يتلقى الطلبات، أما العامل والـ beat فلهما دورة حياة خاصة بهما.

أخطاء شائعة يقع فيها المطورون

من أكثر الأخطاء شيوعًا: نسيان ملف celery.py أو نسيان استيراد celery_app داخل __init__.py.
خطأ شائع آخر هو تشغيل worker على إعدادات غير صحيحة، فيبدو كأنه يعمل، لكنه لا يكتشف المهام.
ومن الأخطاء أيضًا وضع بيانات ضخمة داخل task payload أو الاعتماد على Redis كأنه قاعدة بيانات كاملة لكل شيء دون فهم إعدادات persistence.
خطأ رابع هو عدم ضبط timezone، فتعمل المهمة في ساعة غير متوقعة.
وخطأ خامس هو عدم التفكير في retries والـ idempotency، فتتحول المهام الخلفية إلى مصدر تكرار لا مصدر موثوقية.

مثال عملي أكثر واقعية: نظام إشعارات

فلنتخيل أن لديك نظامًا يرسل إشعارًا للمستخدم كلما تم قبول طلبه. بدلًا من إرسال الإشعار داخل view مباشرة، يمكنك فصل ذلك:

from celery import shared_task

@shared_task
def send_notification(user_id, title, body):
    from core.models import User, Notification

    user = User.objects.get(id=user_id)
    Notification.objects.create(
        user=user,
        title=title,
        body=body,
    )

ثم من الـ view:

def approve_request(request, request_id):
    req = Request.objects.get(id=request_id)
    req.status = "approved"
    req.save()

    send_notification.delay(
        req.user.id,
        "تم قبول طلبك",
        "تمت الموافقة على طلبك بنجاح."
    )

    return JsonResponse({"ok": True})

هذا النمط يتيح لك أيضًا لاحقًا إضافة قناة ثانية مثل البريد الإلكتروني أو SMS دون أن تغيّر تدفق الـ view نفسه. المهم أن تبقى مسؤولية الـ view هي اتخاذ القرار، ومسؤولية Celery هي تنفيذ العمل غير العاجل.

عندما تريد أكثر من مهمة في آن واحد

أحيانًا تحتاج إلى تنفيذ عدة عمليات متوازية ثم جمع النتيجة النهائية. هنا تصبح أدوات Celery المتقدمة مفيدة جدًا. group مناسب عندما تريد إطلاق عدة مهام متشابهة في الوقت نفسه، وchain عندما تكون النتيجة ناتجة عن ترتيب متسلسل، وchord عندما تريد callback بعد انتهاء مجموعة كاملة. وثائق Celery الرسمية تعرض هذه المفاهيم ضمن API الأساسي للتوزيع، وهو ما يجعلها جزءًا أصيلًا من المنظومة وليس إضافة جانبية.

مثال على تسلسل مبسط:

from celery import chain

result = chain(
    fetch_order.s(order_id),
    validate_order.s(),
    charge_customer.s(),
    notify_team.s(),
).delay()

هذا الأسلوب يبدو واضحًا جدًا عند القراءة، وهو أهم من كونه “ذكيًا”؛ لأن المشاريع الطويلة العمر تحتاج إلى كود يستطيع إنسان آخر فهمه بعد سنة، لا فقط كود يعمل في اللحظة الحالية.

لماذا لا يكفي Django وحده؟

Django إطار ويب قوي ومنظم، لكن وظيفته الأساسية ليست أن يكون مدير طوابير مهام. Django ممتاز في بناء التطبيقات وتنظيم النماذج والعروض والقوالب والـ forms والـ ORM، لكن عندما تنتقل من منطق الطلب الفوري إلى منطق الخلفية والجدولة والتوزيع، فأنت تحتاج طبقة متخصصة. وهنا يكون الجمع بين Django وCelery وRedis منطقيًا جدًا: Django يستقبل الطلبات وينظمها، Celery يدير تنفيذ الأعمال الخلفية، وRedis ينقل الرسائل أو يحفظ النتائج بسرعة عالية. هذا التكامل هو ما يصنع الفرق بين مشروع يعمل ومشروع يتوسع بثقة.

صورة ذهنية بسيطة تحفظ الفكرة

فكّر في Django على أنه مكتب الاستقبال.
وفكّر في Celery على أنه غرفة العمليات.
وفكّر في Redis على أنه لوحة التذاكر السريعة التي تنقل المهام وتُنظّمها.

الزبون يأتي إلى الاستقبال، يحصل على تأكيد سريع، ثم تنتقل الأوراق إلى غرفة العمليات لتُنجز بهدوء. بهذه البساطة يمكن فهم الفكرة، وبهذه البساطة أيضًا ينبغي أن تُصمَّم. عندما تنجح في فصل الاستقبال عن التنفيذ، يصبح النظام أكثر سلاسة، وأكثر احتمالًا للأخطاء، وأكثر احترامًا لوقت المستخدم.

خاتمة

استخدام Celery وRedis في Django ليس مجرد تحسين تقني، بل هو تغيير في طريقة التفكير. بدلًا من أن تجعل التطبيق ينتظر كل شيء داخل الطلب نفسه، تمنحه القدرة على توزيع العمل، وعزل المهام الثقيلة، وتخزين النتائج عند الحاجة، وتنفيذ الأعمال الدورية من غير تدخل المستخدم. Celery وفق الوثائق الرسمية هي طابور مهام موزع يركز على المعالجة اللحظية مع دعم الجدولة، وRedis يمكن أن يكون broker وbackend وسريعًا جدًا في نقل الرسائل، بينما Celery beat يتكفل بالمهام الدورية. وعندما تفهم هذه الأدوار جيدًا، ستكتب كودًا أوضح، وتبني تجربة مستخدم أفضل، وتستعد للنمو دون أن تتورط في تعقيد غير ضروري.

في النهاية، أفضل ما في هذا الثنائي أنه لا يفرض عليك الاستعراض؛ بل يمنحك هدوءًا هندسيًا جميلًا. الطلبات تبقى سريعة، والمهام الثقيلة تجد مكانها الطبيعي، والفريق ينام مطمئنًا أكثر لأن النظام صار يفهم أن ليس كل شيء يجب أن يحدث الآن. وهذا، بصراحة، أحد أكثر الأشياء التي تجعل بناء البرمجيات ممتعًا: أن تُقسِّم الفوضى إلى طبقات مفهومة، ثم تجعل كل طبقة تؤدي دورها بصمت وثقة.

#Django #Celery #Redis #background tasks #task queue #Python #Django Celery #Redis broker #Celery beat #async tasks #sending emails #scheduled tasks #retry tasks #workers #message broker

اشترك في نشرتنا البريدية

12k+

المشتركون

أسبوعيًا

التكرار

مجاني

دائمًا