fix(events): keep registration status reads nonblocking
This commit is contained in:
18
config/api_errors.py
Normal file
18
config/api_errors.py
Normal 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
|
||||
@@ -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'),
|
||||
|
||||
@@ -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 *
|
||||
|
||||
35
config/tests/unit/test_api_errors.py
Normal file
35
config/tests/unit/test_api_errors.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user