""" تنظیماتِ gunicorn — خودِ gunicorn این فایل را از پوشه‌ی کاری (WorkingDirectory=~/mathapp-v2 در unitِ systemd) خودکار می‌خواند، پس بدونِ دست زدن به /etc با همان deploy.sh به سرور می‌رسد. آرگومان‌های خطِ فرمانِ unit (مثلِ `--workers 3` و `--bind`) بر این فایل مقدم‌اند. چرا لازم شد (۲۰۲۶-۰۹-۲۴): unit با workerهای sync و timeoutِ پیش‌فرضِ ۳۰ ثانیه اجرا می‌شد. هر استریمِ «از پی بپرس» یک worker را تا پایانِ جواب (~۱۰ ثانیه، چون GapGPT خودش تا آماده شدنِ کلِ جواب نگه می‌دارد) اشغال می‌کرد؛ سه دانش‌آموزِ هم‌زمان با دستیار یعنی بقیه‌ی اپ — نقشه، آزمون، همه‌چیز — در صف. جوابِ بالای ۳۰ ثانیه هم وسطِ کار کشته می‌شد. gthread هر worker را چند نخی می‌کند: ۳ worker × ۸ نخ = ۲۴ درخواستِ هم‌زمان. کدِ جنگو برای نخ امن است و SQLite با `OPTIONS.timeout=20` نوشتنِ هم‌زمان را تحمل می‌کند. timeout در gthread به ضربانِ خودِ worker است، نه طولِ هر درخواست، پس استریمِ طولانی دیگر کشته نمی‌شود. ۳۲ نخ: هر جوابِ دستیار یک نخ را تا آخرِ جواب نگه می‌دارد (روی سرور میانه‌ی ۳ تا ۶ ثانیه، گاهی ۴۰). gthread اتصال را حتی وقتی همه‌ی نخ‌هایش مشغول‌اند می‌پذیرد و workerها یکنواخت پر نمی‌شوند، پس با نخِ کم، صفحه‌های عادیِ اپ پشتِ استریم‌های دستیار صف می‌کشند. در لودتستِ ۲۰۲۶-۱۰-۰۴ (لپ‌تاپ، مدل و Qdrantِ شبیه‌سازی‌شده با زمان‌بندیِ سرور) ۳×۳۲ تا ۷۵ پرسشِ هم‌زمان صفحه‌ها را زیرِ ۰٫۱ ثانیه نگه داشت و از ۱۰۰ به بعد صف پیدا شد. CPUِ مشترکِ سرور در این بار سنجیده نشده. نخِ منتظرِ شبکه تقریباً هزینه ندارد؛ اتصالِ دیتابیس را فقط حینِ کار از poolِ همان worker قرض می‌گیرد (config/settings.py)، پس تعدادِ نخ به سقفِ اتصالِ Postgres ربطی ندارد. """ # ۴ worker: هر worker با GIL حداکثر یک هسته است، پس گذرش با تعدادِ worker بالا می‌رود (لپ‌تاپ، ۱۰۰ # کاربرِ هم‌زمان، ۲۰۲۶-۱۰-۱۱: ۳ → ۵۵۰، ۴ → ۶۲۰، ۶ → ۸۱۰ درخواست در ثانیه). ماشین مشترک است: همان روز # بقیه‌ی تیم‌ها ~۵۵٪ِ ۸ هسته را مصرف می‌کردند (load ≈ ۵)، پس ۴ و نه ۶، تا در اوجِ ما سرویسِ دیگران # گرسنه نماند. هر worker ~۱۰۰ مگابایت. بودجه‌ی اتصالِ دیتابیس: ۴ × DB_POOL_MAX_SIZE(۸) + ۶ = ۳۸ از ۵۷. # این عدد فقط وقتی اثر دارد که یونیتِ systemd خودش `--workers` نداشته باشد (خطِ فرمان مقدم است). workers = 4 worker_class = 'gthread' threads = 32 timeout = 120 graceful_timeout = 30 keepalive = 5 # worker بعد از چند هزار درخواست تازه می‌شود تا نشتِ حافظه‌ی احتمالی جمع # نشود؛ jitter نمی‌گذارد هر سه با هم ری‌استارت شوند. max_requests = 2000 max_requests_jitter = 200 def post_fork(server, worker): """ تعدادِ واقعیِ workerها (با `--workers`ِ خطِ فرمانِ unit، که بر این فایل مقدم است) را به جنگو می‌دهد تا /api/health بودجه‌ی اتصالِ دیتابیس را درست بسنجد: workers × DB_POOL_MAX_SIZE باید زیرِ max_connectionsِ Postgres بماند (config/health.py، _db_pool). پیش از بار شدنِ اپ اجرا می‌شود. """ import os os.environ['GUNICORN_WORKERS'] = str(server.cfg.workers) def post_worker_init(worker): """ کتابخانه‌های Qdrant و OpenAI را همان لحظه‌ی بالا آمدنِ worker بار می‌کند، نه سرِ اولین سوالِ دستیار. بدونِ این، اولین پرسش‌ها بعد از هر شروعِ worker (ری‌استارت، یا تعویض با max_requests) چند ثانیه پیش از رسیدن به مدل معطل می‌مانند: روی سرور ~۶ ثانیه در برابرِ ~۱، روی لپ‌تاپ ۱٫۷ ثانیه (import ِ qdrant_client به‌تنهایی ~۰٫۷). ساختنِ کلاینت تماسِ شبکه‌ای ندارد. شکستِ این گرم‌کردن نباید worker را بخواباند (کلِ اپ به‌خاطرِ یک بهینه‌سازی پایین نمی‌آید)؛ ثبت می‌شود و اولین سوال همان مسیرِ عادی را می‌رود. """ from rag.services.config import rag_is_configured if not rag_is_configured(): return try: from rag.services import clients clients.get_openai_client() clients.get_qdrant_client() except Exception: worker.log.exception('گرم کردنِ کلاینت‌های دستیار شکست خورد؛ اولین سوال آن‌ها را می‌سازد.')