Update project
This commit is contained in:
@@ -10,6 +10,7 @@ commandه، نه receiver. باید periodic اجرا بشه (cron / celery beat
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
|
||||
from content.models import NodeProgress
|
||||
@@ -28,15 +29,46 @@ class Command(BaseCommand):
|
||||
)
|
||||
|
||||
count = 0
|
||||
for progress in stale_qs:
|
||||
old_status = progress.status
|
||||
progress.status = 'abandoned'
|
||||
progress.save(update_fields=['status'])
|
||||
status_changed.send(
|
||||
sender='content.NodeProgress', object_id=progress.id,
|
||||
old_status=old_status, new_status='abandoned',
|
||||
event='inactivity_timeout', user_id=progress.user_id,
|
||||
)
|
||||
# ⚛️ atomic (بهازای هر رکورد، نه کل حلقه): این یک job دورهای رو
|
||||
# روی هزاران رکورد میتونه اجرا کنه. اگه کل حلقه یک atomic واحد
|
||||
# باشه، یک خطا روی رکورد هزارم، ابطالِ ۹۹۹ رکورد درستشدهی قبلی رو
|
||||
# هم rollback میکنه (و کل job باید از اول اجرا بشه). بهجاش هر
|
||||
# رکورد (save + signal که StatusChangeLog میسازه) جدا اتمیکه: یک
|
||||
# خطای مقطعی فقط همون رکورد رو نصفهکاره نگه نمیداره، رکوردهای
|
||||
# قبلی/بعدی مستقل میمونن.
|
||||
#
|
||||
# ⚛️🔒 اصلاح race condition (lost update): قبلاً همین آبجکتِ
|
||||
# progressِ خوندهشده از stale_qs (بدونِ قفل) مستقیماً save میشد.
|
||||
# اگه همزمان یک درخواستِ کاربرِ واقعی (touch_node_progress_on_answer
|
||||
# یا update_node_progress_on_lesson_completion در content/services.py،
|
||||
# هر دو با select_for_update) دقیقاً همون ردیف رو بهروزرسانی و
|
||||
# last_activity_at رو تازه میکرد، این کامند میتونست بدونِ اطلاع از
|
||||
# اون تغییرِ تازه، status رو با مقدارِ قدیمیِ در-حافظه به 'abandoned'
|
||||
# برگردونه — یعنی فعالیتِ تازهی کاربر توسط این جاب rollback/overwrite
|
||||
# میشد. حالا برای هر رکورد، فقط id از stale_qs گرفته میشه؛ ردیفِ
|
||||
# واقعی داخل transaction.atomic با select_for_update دوباره خونده و
|
||||
# قفل میشه (دقیقاً همون الگویی که مسیرِ کاربر استفاده میکنه)، و
|
||||
# شرطِ نصفهرها بودن (status='in_progress' و last_activity_at قدیمیتر
|
||||
# از threshold) بعد از گرفتنِ قفل دوباره چک میشه؛ اگه بین queryset
|
||||
# اولیه و گرفتنِ قفل، یک فعالیتِ کاربر همین ردیف رو تازه کرده باشه،
|
||||
# دیگه شرط برقرار نیست و این ردیف نادیده گرفته میشه (نه abandoned).
|
||||
stale_ids = list(stale_qs.values_list('id', flat=True))
|
||||
for progress_id in stale_ids:
|
||||
with transaction.atomic():
|
||||
progress = NodeProgress.objects.select_for_update().get(pk=progress_id)
|
||||
|
||||
if progress.status != 'in_progress' or progress.last_activity_at is None \
|
||||
or progress.last_activity_at >= threshold:
|
||||
continue
|
||||
|
||||
old_status = progress.status
|
||||
progress.status = 'abandoned'
|
||||
progress.save(update_fields=['status'])
|
||||
status_changed.send(
|
||||
sender='content.NodeProgress', object_id=progress.id,
|
||||
old_status=old_status, new_status='abandoned',
|
||||
event='inactivity_timeout', user_id=progress.user_id,
|
||||
)
|
||||
count += 1
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
|
||||
Reference in New Issue
Block a user