مدیریت تسک های زمان بر با Django-Q

فرض کنید نیازه یک View در پروژه Django خودتون تعریف کنید که کاری که باید انجام بده خیلی طولانی مدته، مثلا وظیفه داره یکسری محاسبات سنگینی انجام بده که حدود 30 دقیقه ای زمان می بره. آیا منطقیه که کاربر یک Request به مسیر مشخص شده ارسال کنه و 30 دقیقه صبر کنه تا Response رو دریافت کنه؟

سلام، من مجید هستم و میخوام در مورد چالش بالا بیشتر صحبت کنم.

احتمالا در بیشتر موارد جواب شما به سوالی که پرسیدم نه باشه. خوب پس باید چه کار کنیم؟ در Django، وقتی تابع View شما اجرا میشه، همه ی کدهای اون در یک Thread اتفاق میافته. یعنی هر خط کد، یکی پس از دیگری اجرا میشه. ولی ما می خواهیم کد طولانی مدت مون رو در یک Thread متفاوت قرار دهیم تا کاربر مجبور نشه مدت زمان زیادی رو صرف کنه تا Response رو دریافت کنه. در واقع میخواییم یه همچین کاری رو انجام بدیم:

# views.py

def my_view(request):
    run_offline(long_running_task) # different thread
    # ...
    return HttpResponse(&quotYour will task be finished soon!&quot)

یکی از روش ها اینه که طوری View مد نظرمون رو بنویسیم که وقتی Request کاربر رو دریافت کرد، همون موقع یه Response به کاربر بفرسته و بگه "کار شما به زودی تموم میشه" و در Background محاسبات سنگین رور انجام بده.

در Django از روش های مختلفی میشه برای پیاده سازی این قابلیت استفاده کرد. استفاده از Celery یاDjango Redis Queue و Django Q از مشهورترین روش ها هستند. برای اکثر پروژه ها، به دلایلی که در ادامه این پست اشاره میکنم، استفاده از Django Q را توصیه می کنم.


وقتی توی اینترنت شروع به جست و جو کنید، اکثرا استفاده از Celery رو به شما پیشنهاد میدن.

درواقع Celery یک صف کار/صف کار ناهمزمان بر اساس ارسال پیام توزیع شده است. بر روی عملیات بلادرنگ متمرکز است، اما از زمان‌بندی نیز پشتیبانی می‌کند.

چی شد؟ ناهمزمانی؟ توزیع شده؟ پیچیده به نظر میاد. آیا من به اینا نیاز دارم؟ Celery برای مبتدیا میتونه ترسناک باشه. من معتقدم که استفاده از Celery برای اکثر پروژه ها زیاده رویه.

یکی از دغدغه ها اینه که Celery نیاز داره که نوعی کارگزار رو راه اندازی کنید(برنامه ای که تمام کارهایی را که باید انجام بشه رو پیگیری کنه). برای اجرای Celery باید برنامه های مثل Redis یا RabbitMQ رو نصب و اجرا کنید، که شروع کار رو پیچیده تر می کنه و زیرساخت های بیشتری را برای نگرانی در اختیار شما قرار می ده.

پیشنهاد من استفاده از Django Q ست که راه‌اندازی و اجرای اون ساده‌تر از Celery هست، و برای سرویس های متوسط رو به پایین کاملاً مناسبه. Django Q می تونه فقط از پایگاه داده موجود شما به عنوان کارگزار استفاده کنه (از طریق ORM خود Django)، به این معنی که شما نیازی به راه اندازی زیرساخت جدیدی ندارید. اگر هم بعداً متوجه شدید که باید از یک کارگزار دیگری (مثل Redis) استفاده کنید، می توانید پایگاه داده را با چیز دیگری تعویض کنید. خیلی هم عالی.


توی این بخش میخواهیم ببینیم که این Django Q چطوری راه اندازی میشه (البته که اگه سراغ داکیومنت اصلی برین بهتره). در اولین گام پکیج مربوطه رو نصب می کنیم:

pip install django-q

سپس باید تنظیمات Django را طوری تنظیم کنیم که پروژه بدونه که باید از برنامه Django Q استفاده کنه. همچنین باید Django Q را پیکربندی کنیم تا از پایگاه داده ی ما به عنوان کارگزار استفاده کنه.

# my_project/settings.py

# Add Django Q to your installed apps.
INSTALLED_APPS = [
    # ...
    'django_q'
]

# Configure your Q cluster
Q_CLUSTER = {
    &quotname&quot: &quotTest&quot,
    &quotorm&quot: &quotdefault&quot,  # Use Django's ORM + database for broker
}

حالا باید جداول Django Q رو بسازیم پس:

python manage.py migrate

حالا فرض کنید جدولی از یکسری کالا برای هر کاربر داریم که میخوایم وقتی کاربر به مسیر مشخص شده درخواستی میزنه، قیمت های هر رکورد (هر کالا) از یک سرویس دیگه ای دریافت و بروز رسانی بشن. پس Model ما یه چیزی شبیه به اینه:

# my_app/models.py

class Stock(models.Model):
    code = models.CharField(max_length=16)
    price = models.DecimalField(decimal_places=2, max_digits=7)
    user = models.ForeignKey(User, on_delete=models.CASCADE)

خب برای اینکه کدی که قراره عملیات طولانی مدتی رو انجام بده رو بنویسیم (در اینجا درخواست برای دریافت قیمت از یک سرویس دیگه)، یک فایل به اسم tasks.py در پوشه Application مون ایجاد میکنیم و کدش رو مینویسیم مثلا:

# my_app/task.py

def refresh_stocks_task(stock_ids):
    stocks = Stocks.objects.filter(id__in=stock_ids).all()
    for stock in stocks:
        stock.price = some_api.fetch_price(stock.code)
        stock.save()

حالا بریم سراغ کد View:

# my_app/views.py

from django_q.tasks import async_task
from .tasks import refresh_stocks_task

def refresh_stocks_view(request):
    stocks = Stocks.objects.filter(user=request.user)
    stock_ids = stocks.values_list('id', flat=True)
    
    # Dispatch task to Django Q
    async_task(refresh_stocks_task, stock_ids)

    return render(request, 'stocks.html', {'stocks': stocks})

همونطور که مشاهده میکنید باید از تابع async_task برای فراخوانی تابع refresh_socks_task استفاده بشه تا این تابع در یک thread مجزا اجرا بشه و سریعا ادامه کد که نمایش صفحه socks.html هست، انجام بشه. (نوشتن url برای refresh_socks_view و migrate پایگاه داده دیگه با خودتون)

کار ما تموم شده تقریبا، فقط یادتون باشه که باید فرآیند Django Q را مستقل اجرا کنیم. هنگام استفاده از Django، معمولاً یک پردازش دارید که مسئول ارائه درخواست‌های وب است و یک پردازش جداگانه که وظایف Django Q را بر عهده دارد. این دو فرآیند عبارتند از:

درخواست های وب:

python manage.py runserver

وظایف Django Q:

python manage.py qcluster

بنابراین اگر فرمان مدیریت qcluster را اجرا نکنید، Task های مد نظر هرگز اجرا نمیشن.


امیدورام این پست مفید بوده باشه براتون.

راه ارتباطی با من در توییتر.

برگرفته شده از.