Files
MathApp/config/exceptions.py
Zareian feaa5eb540 Postgres connection pool, clean 503 under overload, time indexes, 4 workers
Postgres here allows 60 connections, but each of the 96 gunicorn threads held its
own persistent one: at 100 concurrent users (local Postgres with the same limit)
21% of requests were 500 "remaining connection slots are reserved". Now each worker
has a psycopg pool (8), so the same load gives 0 errors and 714 req/s (was 460).

- A full pool answers 503 + Retry-After (DRF handler and middleware), not 500.
- The AI tutor and the OTP SMS call return their connection before the network wait.
- statement_timeout 30 s, idle-in-transaction 60 s; bad pool settings and the old
  DB_CONN_MAX_AGE stop startup instead of failing every request.
- /api/health dbPool: per-worker pool stats and workers x pool + 6 vs max_connections.
- Indexes on the columns global counters filter by time (weekly leaderboard,
  global SMS cap, global AI cap); the leaderboard recomputes once per expiry.
- Worker count moves into gunicorn.conf.py (4 on a shared 8-core box).
- security doc: Redis item closed (server uses our own Redis since 2026-10-08).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:21:35 +03:30

52 lines
2.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
هندلرِ سفارشیِ exception برای DRF: هندلرِ پیش‌فرض فقط APIException/Http404/
PermissionDenied رو می‌شناسه؛ برای بقیه None برمی‌گردونه و exception خام تا
مدیریتِ خطای جنگو بالا می‌ره که (DEBUG=False) یه صفحه‌ی HTML عمومی می‌ده، نه
JSON. این هندلر اون حالت رو با traceback کامل لاگ می‌کنه و یه پاسخِ JSON
ژنریکِ ۵۰۰ برمی‌گردونه.
"""
import logging
from django.core.exceptions import RequestDataTooBig, SuspiciousOperation
from config.db import SERVER_BUSY_BODY, SERVER_BUSY_RETRY_AFTER, is_pool_timeout, log_server_busy
from rest_framework.response import Response
from rest_framework.views import exception_handler as drf_exception_handler
logger = logging.getLogger(__name__)
def custom_exception_handler(exc, context):
response = drf_exception_handler(exc, context)
if response is not None:
return response
request = context.get('request')
path = getattr(request, 'path', '')
# بدنه‌ی بزرگ‌تر از DATA_UPLOAD_MAX_MEMORY_SIZE (≈ ۲٫۵ مگابایت): Django خودش این را ۴۰۰ می‌کند، ولی
# چون DRF آن را نمی‌شناسد پیش‌تر اینجا «۵۰۰ + ERRORِ لاگ با traceback» می‌شد و هر درخواستِ بزرگ لاگ را پر
# می‌کرد (۲۰۲۶-۱۰-۰۷ روی سرورِ زنده دیده شد). باقیِ SuspiciousOperationها هم همان ۴۰۰ِ Django را می‌گیرند.
# poolِ دیتابیس پر (config/db.py): ۵۰۳ با Retry-After. پیش از خواندنِ request.user پایین‌تر، که اگر
# احرازِ هویت خودش همین خطا را داده بود دوباره منتظرِ pool می‌ماند.
if is_pool_timeout(exc):
log_server_busy(request)
return Response(SERVER_BUSY_BODY, status=503, headers={'Retry-After': str(SERVER_BUSY_RETRY_AFTER)})
if isinstance(exc, RequestDataTooBig):
logger.warning('request_too_large path=%s', path)
return Response({'detail': 'حجمِ درخواست بیش از حدِ مجاز است.'}, status=413)
if isinstance(exc, SuspiciousOperation):
logger.warning('suspicious_operation path=%s type=%s', path, type(exc).__name__)
return Response({'detail': 'درخواستِ نامعتبر.'}, status=400)
user = getattr(request, 'user', None)
user_id = user.id if getattr(user, 'is_authenticated', False) else None
logger.exception(
'unhandled_exception path=%s user_id=%s',
path, user_id,
)
return Response(
{'detail': 'خطای غیرمنتظره‌ای رخ داد؛ لطفاً بعداً دوباره تلاش کنید.'},
status=500,
)