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>
This commit is contained in:
@@ -112,6 +112,33 @@ class APINoStoreMiddleware:
|
||||
return response
|
||||
|
||||
|
||||
class DatabaseBusyMiddleware:
|
||||
"""
|
||||
poolِ دیتابیسِ پر → ۵۰۳ با Retry-After بهجای ۵۰۰ (config/db.py) برای viewهای غیرِ DRF مثلِ /admin/؛
|
||||
viewهای DRF را هندلرِ خطای DRF (config/exceptions.py) جواب میدهد، چون هر خطا را خودش ۵۰۰ میکند
|
||||
و استثنا هیچوقت به اینجا نمیرسد. فقط خطای pool را میگیرد؛ هر OperationalErrorِ دیگری (Postgres
|
||||
پایین، کوئریِ timeoutخورده) همان ۵۰۰ِ قبلی میماند.
|
||||
"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
return self.get_response(request)
|
||||
|
||||
def process_exception(self, request, exception):
|
||||
from django.http import JsonResponse
|
||||
|
||||
from config.db import SERVER_BUSY_BODY, SERVER_BUSY_RETRY_AFTER, is_pool_timeout, log_server_busy
|
||||
|
||||
if not is_pool_timeout(exception):
|
||||
return None
|
||||
log_server_busy(request)
|
||||
response = JsonResponse(SERVER_BUSY_BODY, status=503)
|
||||
response['Retry-After'] = str(SERVER_BUSY_RETRY_AFTER)
|
||||
return response
|
||||
|
||||
|
||||
class AdminLoginThrottleMiddleware:
|
||||
"""
|
||||
سقفِ تلاشِ ورود به /admin/ برای هر IP (ADMIN_LOGIN_MAX_ATTEMPTS در ۱۵ دقیقه).
|
||||
|
||||
Reference in New Issue
Block a user