admin & data: correct admin action redirect, add SQLite retry, seed_test_data command
This commit is contained in:
@@ -9,7 +9,9 @@ retry یا پیگیریِ تحویلی انجام نمیده — طبق Messa
|
||||
فراخوانی همیشه sync و بلاکینگه (برای هر کاربر یک send_sms/notify)؛
|
||||
مناسبِ لیستهای کوچیک یا اجرای دستی/تست، نه کمپینهای بزرگ در پروداکشن.
|
||||
"""
|
||||
from django.db import IntegrityError, transaction
|
||||
import time
|
||||
|
||||
from django.db import IntegrityError, OperationalError, transaction
|
||||
|
||||
from accounts import services as account_services
|
||||
from accounts.services import SMSDeliveryError
|
||||
@@ -17,6 +19,26 @@ from notifications.services import notify
|
||||
|
||||
from .models import CampaignDelivery, MessageCampaign
|
||||
|
||||
|
||||
def _retry_on_sqlite_lock(fn, *, retries=5, base_delay=0.05):
|
||||
"""
|
||||
روی SQLite (مخصوصاً دیتابیسِ حافظهایِ shared-cache که تستها ازش
|
||||
استفاده میکنن)، وقتی دو اتصالِ همزمان میخوان همون جدول رو بنویسن،
|
||||
ممکنه OperationalError('database table is locked') بگیریم. این
|
||||
SQLITE_LOCKEDه نه SQLITE_BUSY، پس busy_timeoutِ معمولیِ اتصال کمکی
|
||||
نمیکنه و بلافاصله raise میشه؛ یک خطای واقعیِ داده نیست، فقط
|
||||
تناقضِ لحظهایِ لاکه. این تابع فقط همین خطای گذرا رو با چند بار
|
||||
تلاشِ سریع (backoffِ کوچیکِ فزاینده) دوباره امتحان میکنه؛ هر
|
||||
OperationalError دیگه (یا شکستِ تلاشِ آخر) دستنخورده بالا میره.
|
||||
"""
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
return fn()
|
||||
except OperationalError as exc:
|
||||
if 'locked' not in str(exc).lower() or attempt == retries - 1:
|
||||
raise
|
||||
time.sleep(base_delay * (attempt + 1))
|
||||
|
||||
# نوعِ نوتیفیکیشنِ ثبتشده برای اعلانهای ناشی از کمپین. عمداً در
|
||||
# notifications.models.Notification.TYPE_CHOICES اضافه نشده چون آن
|
||||
# choices فقط رویدادهاییان که notifications بهعنوانِ event-listener
|
||||
@@ -70,12 +92,12 @@ def _mark_delivery_result(delivery: CampaignDelivery, *, error: Exception | None
|
||||
"""رکوردِ pendingای که قبل از تلاشِ ارسال ساخته شده رو به sent/failed آپدیت میکنه."""
|
||||
if error is None:
|
||||
delivery.status = CampaignDelivery.STATUS_SENT
|
||||
delivery.save(update_fields=['status'])
|
||||
_retry_on_sqlite_lock(lambda: delivery.save(update_fields=['status']))
|
||||
return
|
||||
|
||||
delivery.status = CampaignDelivery.STATUS_FAILED
|
||||
delivery.error_message = str(error)[:300]
|
||||
delivery.save(update_fields=['status', 'error_message'])
|
||||
_retry_on_sqlite_lock(lambda: delivery.save(update_fields=['status', 'error_message']))
|
||||
|
||||
|
||||
def _claim_delivery(campaign: MessageCampaign, user_id: int) -> CampaignDelivery | None:
|
||||
@@ -96,11 +118,14 @@ def _claim_delivery(campaign: MessageCampaign, user_id: int) -> CampaignDelivery
|
||||
همزمانِ دیگر)، create() با IntegrityError رد میشه و این تابع
|
||||
None برمیگردونه — یعنی کاربر بهجای duplicate delivery، skip میشه.
|
||||
"""
|
||||
try:
|
||||
def _create():
|
||||
with transaction.atomic():
|
||||
return CampaignDelivery.objects.create(
|
||||
campaign=campaign, user_id=user_id, channel=campaign.channel,
|
||||
)
|
||||
|
||||
try:
|
||||
return _retry_on_sqlite_lock(_create)
|
||||
except IntegrityError:
|
||||
return None
|
||||
|
||||
@@ -203,7 +228,7 @@ def execute_campaign(campaign: MessageCampaign) -> None:
|
||||
delivery = _claim_delivery(campaign, user.id)
|
||||
if delivery is None:
|
||||
continue
|
||||
try:
|
||||
def _notify_and_mark_sent():
|
||||
with transaction.atomic():
|
||||
notify(
|
||||
user_id=user.id,
|
||||
@@ -213,6 +238,9 @@ def execute_campaign(campaign: MessageCampaign) -> None:
|
||||
related_id=campaign.id,
|
||||
)
|
||||
_mark_delivery_result(delivery, error=None)
|
||||
|
||||
try:
|
||||
_retry_on_sqlite_lock(_notify_and_mark_sent)
|
||||
except Exception as exc:
|
||||
# چه notify() خودش شکست بخوره چه فقط نوشتنِ نهاییِ sent
|
||||
# (که تویِ همون atomic block بالاست)، هر دو حالت اینجا
|
||||
|
||||
Reference in New Issue
Block a user