Files
MathApp/tools/loadtest
Zareian 645baf846b Signed payment links, hashed refresh tokens, login-only leaderboard, load test tools
- /api/payment/orders/{id}/pay requires the short-lived signature that paymentUrl carries;
  without it (or with another order's) it is the same 404 as an unknown order
- Session.refresh_token stores the SHA-256 of the token; migration accounts.0007 hashes the
  existing rows so tokens clients already hold keep working
- /api/leaderboard requires a token (guests have one); names of children were scrapable
- RAG mock only for `manage.py test`, not any command with an argument named "test"
- tools/loadtest: user factory, load generator, fake GapGPT+Qdrant, with method notes
- security-before-production.md lists only open items; closed ones become a guards table
  with the tests that hold them. Comments describe current behaviour, not history

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:12:28 +03:30
..

لودتستِ دستیار

سه اسکریپت، برای اینکه «چند دانش‌آموزِ هم‌زمان» هر بار با یک روش سنجیده شود. خروجیِ هر اجرا یک JSON است با زمانِ هر پرسش و هر درخواستِ صفحه‌ی عادی؛ جمع‌بندی در خودِ خروجیِ ترمینال چاپ می‌شود.

فایل کار
mkusers.py کاربرانِ آزمایشیِ غیرمهمان (شماره‌ی جعلیِ 0900…، بی‌پیامک، بی‌سیگنال) با توکن و سه سوالِ متفاوت از بانکِ سوال برای هر کدام
loadtest.py N نفر هم‌زمان می‌پرسند (استریم، دقیقاً مثلِ فرانت) و B نفرِ دیگر در خانه/آزمون/خلاصه/سهمیه می‌چرخند
fake_upstream.py جایگزینِ GapGPT و Qdrant برای آزمایشگاه: embedding، جوابِ استریم با زمانِ لاگ‌نرمال (پیش‌فرض: میانه‌ی ۵٫۲ و صدکِ ۹۰ِ ۱۳ ثانیه، از جواب‌های واقعیِ سرور) و جست‌وجوی Qdrant

آزمایشگاه (بی‌هزینه، روی لپ‌تاپ)

روی یک کپی از پروژه اجرا کنید تا دیتابیسِ اصلیِ لوکال پر نشود. Qdrant هم شبیه‌سازی می‌شود، چون از لپ‌تاپ پهنای باندِ بالارفتن (هر بردار ~۳۰ کیلوبایت) جست‌وجوها را سریالی نشان می‌دهد؛ از خودِ سرور ۲۰ جست‌وجوی هم‌زمان ۰٫۲۶ ثانیه است.

python tools/loadtest/fake_upstream.py &                      # :8901 (FAKE_SIGMA=0 یعنی همیشه ۵٫۲ ثانیه)
export DJANGO_DEBUG=0 CACHE_URL= NUM_PROXIES=0 AI_DAILY_QUOTA_FREE=1000 HEARTS_ENFORCED=0 RAG_FORCE_MOCK=0 \
       DJANGO_ALLOWED_HOSTS=127.0.0.1 OPENAI_BASE_URL=http://127.0.0.1:8901/v1 OPENAI_API_KEY=x \
       QDRANT_URL=http://127.0.0.1:8901 QDRANT_API_KEY=x
LT_N=150 python manage.py shell -c "exec(open('tools/loadtest/mkusers.py').read())" | grep ^LTJSON | cut -c7- > users.json
gunicorn --bind 127.0.0.1:8800 config.wsgi:application &      # همان gunicorn.conf.py ِ سرور
python tools/loadtest/loadtest.py http://127.0.0.1:8800 users.json 50 10 0 out.json --writes

پیش از هر اجرا gunicorn را از نو بالا بیاورید (کشِ جوابِ هر worker خالی شود) و مطمئن شوید پروسه‌ی قبلی واقعاً رفته است؛ وگرنه سوال‌های تکراری از کش در ۰٫۱ ثانیه جواب می‌گیرند و عدد دروغ می‌گوید.

سرورِ اصلی (هزینه‌ی واقعی: ~۰٫۱۸ سنت برای هر جواب)

۱. بکاپ از دیتابیس (docs/operations.md). ۲. کاربرانِ آزمایشی را روی سرور با mkusers.py بسازید (LT_PREFIX یک پیشوندِ جعلی که روی سرور نیست) و خروجی را فقط در یک فایلِ 600 نگه دارید. ۳. loadtest.py را با https://mathapi.tadrisino2.ir و بدونِ --writes اجرا کنید. ۴. زمانِ داخلِ GapGPT برای هر پیام در ai_tutor.Message.latency_ms است؛ اختلافِ آن با زمانِ کلاینت یعنی کارِ سرورِ ما و صفِ انتظار برای نخِ آزاد. ۵. manage.py delete_users --ids … --yes و پاک کردنِ فایلِ توکن‌ها.

سقفِ ساعتیِ دستیار برای مهمان‌ها بر اساسِ IP است؛ با کاربرِ مهمان از یک ماشین بیش از ~۲۰ پرسش در ساعت ممکن نیست، برای همین mkusers.py کاربرِ غیرمهمان می‌سازد.