Files
MathApp/gunicorn.conf.py
Zareian b3f624a512 gunicorn: warm the assistant's Qdrant/OpenAI clients when a worker starts
The first assistant round after a restart waited ~6 s before reaching the model on the
live server (later rounds ~1 s); locally a cold 10-ask burst took 7.5 s vs 6.1 s with
this hook (simulated model, 5.2 s). Workers are recycled by max_requests, so the delay
recurred. A failed warm-up is logged and does not stop the worker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 14:41:48 +03:30

61 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
تنظیماتِ 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 است، نه طولِ هر
درخواست، پس استریمِ طولانی دیگر کشته نمی‌شود.
۸ → ۳۲ نخ (۲۰۲۶-۱۰-۰۴، لودتست): هر جوابِ دستیار یک نخ را تا آخرِ جواب نگه می‌دارد
(روی سرور میانه ۵ و p90 ۱۳ ثانیه). با ۳×۸ از حدودِ ۲۰ پرسشِ هم‌زمان، صفحه‌های عادیِ اپ
پشتِ استریم‌ها صف کشیدند (تا ۲۳ ثانیه در ۵۰ پرسش؛ فرانت بعد از ۱۵ ثانیه خطا می‌دهد)، چون
gthread اتصال را حتی وقتی همه‌ی نخ‌هایش مشغول‌اند می‌پذیرد و workerها یکنواخت پر نمی‌شوند.
با ۳×۳۲ تا ۷۵ پرسشِ هم‌زمان صفحه‌ها زیرِ ۱۱۰ میلی‌ثانیه ماندند. این روی لپ‌تاپ با مدل و
Qdrantِ شبیه‌سازی‌شده سنجیده شد؛ CPUِ مشترکِ سرور و رفتارِ GapGPT زیرِ این بار جدا
سنجیده نشده‌اند. نخِ منتظرِ شبکه تقریباً هزینه ندارد؛ هر نخ اتصالِ SQLiteِ خودش را دارد.
"""
worker_class = 'gthread'
threads = 32
timeout = 120
graceful_timeout = 30
keepalive = 5
# worker بعد از چند هزار درخواست تازه می‌شود تا نشتِ حافظه‌ی احتمالی جمع
# نشود؛ jitter نمی‌گذارد هر سه با هم ری‌استارت شوند.
max_requests = 2000
max_requests_jitter = 200
def post_worker_init(worker):
"""
کتابخانه‌های Qdrant و OpenAI را همان لحظه‌ی بالا آمدنِ worker بار می‌کند، نه سرِ اولین
سوالِ دستیار. در لودتستِ ۲۰۲۶-۱۰-۰۴ اولین دورِ پرسش بعد از ری‌استارت روی سرور ~۶ ثانیه
پیش از رسیدن به مدل معطل ماند و دورهای بعد ~۱ ثانیه؛ روی لپ‌تاپ همین تفاوت ۱٫۷ ثانیه بود
(import ِ qdrant_client به‌تنهایی ~۰٫۷ ثانیه). با max_requests هر worker گاه‌به‌گاه از نو
بالا می‌آید، پس بدونِ این هر چند هزار درخواست یک دانش‌آموز همین تأخیر را می‌دید.
ساختنِ کلاینت تماسِ شبکه‌ای ندارد. شکستِ این گرم‌کردن نباید 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('گرم کردنِ کلاینت‌های دستیار شکست خورد؛ اولین سوال آن‌ها را می‌سازد.')