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:
18
ai_tutor/migrations/0008_alter_message_created_at.py
Normal file
18
ai_tutor/migrations/0008_alter_message_created_at.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.17 on 2026-10-11 16:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ai_tutor', '0007_message_charge'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='message',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, db_index=True),
|
||||
),
|
||||
]
|
||||
@@ -30,7 +30,9 @@ class Message(models.Model):
|
||||
# به API نرسید). سهمیهی رایگانِ امروز = تعدادِ «free»های امروز.
|
||||
CHARGE_CHOICES = [('free', 'رایگانِ روزانه'), ('credit', 'اعتبارِ بسته'), ('none', 'بیهزینه')]
|
||||
charge = models.CharField(max_length=6, choices=CHARGE_CHOICES, blank=True, default='')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
# ایندکس: سقفِ روزانهی کلِ سامانه (_check_global_daily_cap) در هر سوال پیامهای امروزِ همه را
|
||||
# میشمارد؛ این سریعترین جدولِ در حالِ رشد است.
|
||||
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||
|
||||
# --- حاشیهنویسیِ تولید (فقط برای پیامهای role='assistant') --------------
|
||||
# سینیور خواست لایک/دیسلایک بعداً قابلِ تحلیل باشد: «کدام جواب بهتر بوده».
|
||||
|
||||
@@ -16,6 +16,7 @@ from rest_framework.throttling import ScopedRateThrottle
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from accounts.models import User as AccountUser
|
||||
from config.db import release_db_connection
|
||||
from . import provider_status
|
||||
from .models import AskIdempotency, Conversation, Message, MessageFeedback
|
||||
from .serializers import (
|
||||
@@ -471,6 +472,8 @@ class AskAIView(APIView):
|
||||
answer_from_cache = True
|
||||
_uncharge(user_message)
|
||||
else:
|
||||
# Qdrant و مدل چند ثانیه طول میکشند؛ اتصالِ دیتابیس در این مدت به pool برمیگردد.
|
||||
release_db_connection()
|
||||
used_chunks, retrieval_meta = retrieve_context(
|
||||
_retrieval_query(v['userQuestion'], history, focus),
|
||||
subject=subject, grade_id=grade_id,
|
||||
@@ -610,6 +613,8 @@ class AskAIView(APIView):
|
||||
_uncharge(user_message)
|
||||
yield _sse('chunk', {'delta': answer_text})
|
||||
else:
|
||||
# کلِ استریم (تا ۴۰ ثانیه) بیاتصالِ دیتابیس؛ ثبتِ پیامِ آخر یکی تازه قرض میگیرد.
|
||||
release_db_connection()
|
||||
used_chunks, retrieval_meta = retrieve_context(
|
||||
_retrieval_query(v['userQuestion'], history, focus),
|
||||
subject=subject, grade_id=grade_id,
|
||||
|
||||
Reference in New Issue
Block a user