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:
@@ -17,7 +17,7 @@ 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 . import concurrency, provider_status
|
||||
from .models import AskIdempotency, Conversation, Message, MessageFeedback
|
||||
from .serializers import (
|
||||
QuotaResponseSerializer,
|
||||
@@ -256,6 +256,15 @@ def generate_tutor_answer(
|
||||
return answer, follow_ups
|
||||
|
||||
|
||||
class AIBusy(APIException):
|
||||
"""همهی جاهای همزمانِ دستیار در این worker پرند (ai_tutor/concurrency.py). سهمیه دست نخورده."""
|
||||
status_code = 503
|
||||
default_detail = 'دستیار الان خیلی شلوغه؛ چند ثانیهی دیگه دوباره بپرس.'
|
||||
default_code = 'assistant_busy'
|
||||
# هندلرِ DRF از `wait` هدرِ Retry-After میسازد.
|
||||
wait = 10
|
||||
|
||||
|
||||
class AskAIView(APIView):
|
||||
"""POST /api/ai/ask — سوال از AI (استریم اختیاری؛ در این فاز استریم پیاده نشده)"""
|
||||
|
||||
@@ -282,6 +291,24 @@ class AskAIView(APIView):
|
||||
tags=['AI Tutor (ai_tutor)'],
|
||||
)
|
||||
def post(self, request):
|
||||
# پیش از هر کارِ دیگری، حتی رزروِ سهمیه: درخواستِ ردشده نباید از سهمیهی روزانه کم کند.
|
||||
if not concurrency.try_acquire():
|
||||
logger.warning('ai_busy user_id=%s in_flight=%s limit=%s',
|
||||
request.user.id, concurrency.in_flight(), concurrency.limit())
|
||||
raise AIBusy()
|
||||
try:
|
||||
response = self._ask(request)
|
||||
except BaseException:
|
||||
concurrency.release()
|
||||
raise
|
||||
if isinstance(response, StreamingHttpResponse):
|
||||
# جا تا پایانِ استریم دستِ همین درخواست میماند (ReleaseWhenDone).
|
||||
response.streaming_content = concurrency.ReleaseWhenDone(response.streaming_content)
|
||||
else:
|
||||
concurrency.release()
|
||||
return response
|
||||
|
||||
def _ask(self, request):
|
||||
serializer = AskRequestSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
v = serializer.validated_data
|
||||
|
||||
Reference in New Issue
Block a user