fix(events): keep registration status reads nonblocking

This commit is contained in:
2026-10-01 13:35:17 +03:30
parent 962238c32c
commit f924fcc52e
16 changed files with 498 additions and 107 deletions

18
config/api_errors.py Normal file
View File

@@ -0,0 +1,18 @@
"""HTTP handling for recoverable database errors."""
from django.http import JsonResponse
def handle_database_operational_error(request, error):
"""Return a retryable response when PostgreSQL rejects a lock wait."""
cause = error.__cause__
while cause is not None:
if getattr(cause, "sqlstate", None) == "55P03" or getattr(cause, "pgcode", None) == "55P03":
response = JsonResponse(
{"detail": "درخواست به‌دلیل شلوغی موقت پایگاه داده انجام نشد؛ کمی بعد دوباره تلاش کنید."},
status=503,
)
response["Retry-After"] = "2"
return response
cause = cause.__cause__
raise error

View File

@@ -48,6 +48,11 @@ app.conf.beat_schedule = {
'schedule': crontab(minute='*'),
'description': 'Releases seats from paid checkouts after their 15-minute window.',
},
'deliver-registration-notifications': {
'task': 'apps.events.deliver_pending_registration_notifications',
'schedule': crontab(minute='*'),
'description': 'Retries committed registration notifications.',
},
'sweep-pending-refunds': {
'task': 'apps.payments.sweep_pending_refunds',
'schedule': crontab(minute='*/5'),

View File

@@ -249,6 +249,15 @@ BLOG_ASSET_MAX_SIZE_MB = config('BLOG_ASSET_MAX_SIZE_MB', default=50, cast=int)
BLOG_IMAGE_ASSET_MAX_SIZE_MB = config('BLOG_IMAGE_ASSET_MAX_SIZE_MB', default=10, cast=int)
if DATABASES["default"]["ENGINE"] == "django.db.backends.postgresql":
lock_timeout_ms = config("DB_LOCK_TIMEOUT_MS", default=5000, cast=int)
idle_transaction_timeout_ms = config("DB_IDLE_TRANSACTION_TIMEOUT_MS", default=60000, cast=int)
database_options = DATABASES["default"].setdefault("OPTIONS", {})
existing_options = database_options.get("options") or ""
timeout_options = (
f"-c lock_timeout={lock_timeout_ms}ms "
f"-c idle_in_transaction_session_timeout={idle_transaction_timeout_ms}ms"
)
database_options["options"] = f"{existing_options} {timeout_options}".strip()
DATABASES["default"]["ENGINE"] = "django_prometheus.db.backends.postgresql"
from config.services.unfold import *

View File

@@ -0,0 +1,35 @@
import json
import pytest
from django.db import OperationalError
from config.api_errors import handle_database_operational_error
pytestmark = pytest.mark.unit
class PostgresError(Exception):
def __init__(self, sqlstate):
super().__init__(sqlstate)
self.sqlstate = sqlstate
def test_database_lock_timeout_returns_retryable_service_unavailable():
error = OperationalError("canceling statement due to lock timeout")
error.__cause__ = PostgresError("55P03")
response = handle_database_operational_error(None, error)
assert response.status_code == 503
assert response["Retry-After"] == "2"
assert json.loads(response.content) == {
"detail": "درخواست به‌دلیل شلوغی موقت پایگاه داده انجام نشد؛ کمی بعد دوباره تلاش کنید."
}
def test_unrelated_database_operational_error_is_not_hidden():
error = OperationalError("database unavailable")
error.__cause__ = PostgresError("08006")
with pytest.raises(OperationalError, match="database unavailable"):
handle_database_operational_error(None, error)

View File

@@ -1,16 +1,19 @@
from django.contrib import admin
from django.db import OperationalError
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from ninja import NinjaAPI
from apps.notifications.api.views import NotificationStreamView
from config.api import router as api_router
from config.api_errors import handle_database_operational_error
api = NinjaAPI(
title="CS Association API",
version="1.0.0",
description="API for University Computer Science Association",
)
api.add_exception_handler(OperationalError, handle_database_operational_error)
api.add_router("", api_router)