""" سقفِ جواب‌های هم‌زمانِ دستیار در هر worker (AI_MAX_CONCURRENT_PER_WORKER). جوابِ دستیار یک نخِ gunicorn را تا پایانِ کار (۳ تا ۴۰ ثانیه، منتظرِ Qdrant و مدل) نگه می‌دارد. بدونِ سقف، اگر چند ده دانش‌آموز با هم سوال بپرسند همه‌ی نخ‌های worker گرفته می‌شوند و نقشه، آزمون و ثبتِ جواب (که هرکدام چند میلی‌ثانیه‌اند) پشتِ آن‌ها صف می‌کشند: یک بخشِ کند کلِ اپ را می‌خواباند. این همان الگوی bulkhead است: هر بخش سهمِ جدای خودش را دارد. درخواستی که جا نگیرد فوراً ۵۰۳ می‌گیرد. سمافور مالِ همین پروسه است (هر worker جدا). هم‌زمانیِ واقعی زیرِ بارِ gunicorn با مدلِ واقعی آزموده نشده؛ تست‌ها رفتارِ گرفتن/پس‌دادن را در مسیرِ JSON و استریم نگه می‌دارند (ai_tutor/tests_concurrency.py). """ import threading from django.conf import settings _lock = threading.Lock() _in_flight = 0 def limit() -> int: return settings.AI_MAX_CONCURRENT_PER_WORKER def try_acquire() -> bool: """بی‌انتظار: یا جا هست و گرفته می‌شود، یا False. انتظار نخ را همان‌طور نگه می‌داشت که می‌خواهیم نشود.""" global _in_flight with _lock: if _in_flight >= limit(): return False _in_flight += 1 return True def release() -> None: global _in_flight with _lock: if _in_flight <= 0: # پس‌دادنِ بی‌گرفتن یعنی باگ در جفت‌شدنِ گرفتن/پس‌دادن؛ شمارنده منفی نمی‌شود تا سقف بی‌اثر نشود. raise RuntimeError('ai concurrency slot released more times than acquired') _in_flight -= 1 def in_flight() -> int: return _in_flight class ReleaseWhenDone: """ محتوای استریم را می‌پیچد تا جا وقتی پس داده شود که استریم واقعاً تمام شد: یا با پایانِ خواندن، یا با close() که جنگو هنگامِ بستنِ پاسخ صدا می‌زند (کاربر وسطِ جواب صفحه را بست، یا پاسخ اصلاً خوانده نشد). generatorی که شروع نشده با close() بلوکِ finallyاش را اجرا نمی‌کند؛ برای همین پس‌دادن این‌جاست، نه داخلِ generator. """ def __init__(self, iterable): self._iterable = iterable self._released = False def _release_once(self): if not self._released: self._released = True release() def __iter__(self): try: yield from self._iterable finally: self._release_once() def close(self): try: close = getattr(self._iterable, 'close', None) if close is not None: close() finally: self._release_once()