Spike readiness: AI gets its own slice of threads, cheaper hot paths, classroom-friendly login

- /api/ai/ask: at most AI_MAX_CONCURRENT_PER_WORKER (12 of 32 threads) answers per worker;
  beyond that 503 assistant_busy + Retry-After before any quota is reserved. Without it a
  burst of questions (3-40 s each) took every thread and the map and quizzes queued behind.
  The slot is held until the stream is actually finished or closed.
- Answer submission 21 -> 14 queries, 15.4 -> 11.0 ms: one query for all choices, locked
  get_or_create for node progress and streak, and today's streak recorded once per day
  via the cache instead of a transaction per answer.
- /api/home 12.4 -> 8.6 ms: the user's completions are read by user_id instead of an IN list
  of ~600 lesson ids; the dict is returned directly (serializer stays the Swagger contract,
  HomeMatchesItsSerializerTests keeps them equal).
- otp_login per-IP 30 -> 300/hour: a class of 30 on one school Wi-Fi filled it; guessing is
  bounded per code and per phone.
- /api/health aiConcurrency.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 20:55:24 +03:30
parent feaa5eb540
commit 2c708088e4
11 changed files with 279 additions and 19 deletions

View File

@@ -552,7 +552,9 @@ REST_FRAMEWORK = {
# می‌توانستند وارد شوند. حفاظِ اصلی per-phone و per-code است (پایین‌تر و
# در دیتابیس)؛ این‌ها فقط جلوی یک ماشینِ تنها را می‌گیرند.
'otp_request': os.environ.get('OTP_REQUEST_IP_RATE', '20/hour'),
'otp_login': os.environ.get('OTP_LOGIN_IP_RATE', '30/hour'),
# ۳۰ تا ۲۰۲۶-۱۰-۱۱: یک کلاسِ سی‌نفره روی وای‌فایِ مدرسه با چند غلطِ تایپی سقف را پر می‌کرد. حدسِ کد
# را سقفِ هر کد (OTP_MAX_FAILED_ATTEMPTS=۵، در دیتابیس) و هر شماره می‌بندد، نه این.
'otp_login': os.environ.get('OTP_LOGIN_IP_RATE', '300/hour'),
'bazaar_verify': '10/hour',
'bazaar_oauth': '20/hour',
'ai_ask': '20/hour',
@@ -929,6 +931,11 @@ AI_DAILY_QUOTA_FREE = int(os.environ.get('AI_DAILY_QUOTA_FREE', '2'))
# تازه می‌توانست خرجِ GapGPT را بی‌سقف بالا ببرد. ۳۰۰۰ با ~۰٫۲ سنت برای هر سوال یعنی حدودِ ۶
# دلار در روز؛ رسیدن به آن ۵۰۳ و لاگِ ERROR می‌دهد. با رشدِ کاربرِ واقعی از .env بالا ببرید.
AI_GLOBAL_DAILY_MAX = int(os.environ.get('AI_GLOBAL_DAILY_MAX', '3000'))
# حداکثر جوابِ هم‌زمانِ دستیار در هر worker (ai_tutor/concurrency.py). هر جواب یک نخ را ۳ تا ۴۰ ثانیه
# نگه می‌دارد؛ بی‌سقف، رگبارِ سوال هر ۳۲ نخِ worker را می‌گرفت و نقشه و آزمون پشتش صف می‌کشیدند. با ۱۲،
# دست‌کم ۲۰ نخ برای بقیه‌ی اپ می‌ماند؛ ۴ worker × ۱۲ = ۴۸ جوابِ هم‌زمان. بیشتر از آن ۵۰۳ «شلوغ است»
# می‌گیرد، پیش از رزروِ سهمیه. سقفِ هم‌زمانیِ خودِ GapGPT را نمی‌دانیم و نسنجیده‌ایم.
AI_MAX_CONCURRENT_PER_WORKER = int(os.environ.get('AI_MAX_CONCURRENT_PER_WORKER', '12'))
# اختیاری: مدلِ جدا فقط برای پایه‌های دهم تا دوازدهم (حسابان/مثلثات). خالی یعنی
# همه‌ی پایه‌ها OPENAI_CHAT_MODEL. انتخابش با `rag_model_benchmark` سنجیده می‌شود.
OPENAI_CHAT_MODEL_UPPER = os.environ.get('OPENAI_CHAT_MODEL_UPPER', '')