Files
MathApp/.env.example
Zareian feaa5eb540 Postgres connection pool, clean 503 under overload, time indexes, 4 workers
Postgres here allows 60 connections, but each of the 96 gunicorn threads held its
own persistent one: at 100 concurrent users (local Postgres with the same limit)
21% of requests were 500 "remaining connection slots are reserved". Now each worker
has a psycopg pool (8), so the same load gives 0 errors and 714 req/s (was 460).

- A full pool answers 503 + Retry-After (DRF handler and middleware), not 500.
- The AI tutor and the OTP SMS call return their connection before the network wait.
- statement_timeout 30 s, idle-in-transaction 60 s; bad pool settings and the old
  DB_CONN_MAX_AGE stop startup instead of failing every request.
- /api/health dbPool: per-worker pool stats and workers x pool + 6 vs max_connections.
- Indexes on the columns global counters filter by time (weekly leaderboard,
  global SMS cap, global AI cap); the leaderboard recomputes once per expiry.
- Worker count moves into gunicorn.conf.py (4 on a shared 8-core box).
- security doc: Redis item closed (server uses our own Redis since 2026-10-08).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:21:35 +03:30

143 lines
10 KiB
Plaintext
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.
# نمونه‌ی تنظیماتِ بک‌اندِ تدریسینو — برای لپ‌تاپ و سرور.
#
# cp .env.example .env
#
# هیچ‌کدام برای بالا آمدنِ سرویس اجباری نیست: دامنه‌ها، CORS، کلیدِ امضا و کش در
# خودِ config/settings.py پیش‌فرضِ کارا دارند. مقدارهای این فایل با آن پیش‌فرض‌ها
# ادغام می‌شوند، جایگزینشان نمی‌شوند. شرحِ کامل: docs/operations.md.
#
# مقدارِ واقعیِ کلیدها فقط در .env (که در git نیست)، هرگز در این فایل یا در چت.
# ═══════════════════════════════════════════════════════════════════════
# ۱) پایه
# ═══════════════════════════════════════════════════════════════════════
# روی لپ‌تاپ 1، روی سرور همیشه 0.
# DEBUG=1 روی سرور یعنی کدِ ورودِ 0000 برای هر شماره‌ای و صفحه‌ی خطا با تنظیمات.
DJANGO_DEBUG=0
# خالی بماند: بارِ اول یک کلیدِ تصادفی ساخته و در .django_secret_key (0600) نگه
# داشته می‌شود. هرگز مقدارِ django-insecure-… داخلِ settings.py را این‌جا نگذارید.
DJANGO_SECRET_KEY=
# Redis برای محدودیتِ نرخِ مشترک بینِ workerها؛ خالی = کشِ حافظه‌ی هر worker
# (/api/health هشدار می‌دهد). مثال: redis://127.0.0.1:6379/1
CACHE_URL=
# Postgres: با ست‌بودنِ DB_NAME برنامه روی Postgres بالا می‌آید (DB_USER و DB_PASSWORD الزامی)؛ خالی = SQLite.
DB_NAME=
DB_USER=
DB_PASSWORD=
DB_HOST=127.0.0.1
DB_PORT=5432
# pool اتصال‌ها (config/settings.py): هر worker حداکثر DB_POOL_MAX_SIZE اتصال؛ بعد از DB_POOL_TIMEOUT ثانیه انتظار ۵۰۳.
# workers × DB_POOL_MAX_SIZE + 6 باید زیرِ max_connectionsِ Postgres بماند (/api/health → dbPool).
# DB_POOL_MAX_SIZE=8
# DB_POOL_TIMEOUT=10
# سقفِ هر کوئری (میلی‌ثانیه)؛ 0 برای فرمانِ مدیریتیِ طولانیِ دستی.
# DB_STATEMENT_TIMEOUT_MS=30000
# اسلاتِ تکثیری که سرورِ بکاپ از آن کپیِ زنده می‌گیرد (ops/pgbackup/). روی سرور: zareian_247.
# خالی روی Postgres = کپیِ بیرونی نداریم و /api/health هشدار می‌دهد. روی لپ‌تاپ خالی.
OFFSITE_WAL_SLOT=
# ═══════════════════════════════════════════════════════════════════════
# ۲) دامنه‌ها — دو آدرس، بقیه از آن‌ها ساخته می‌شوند
# ═══════════════════════════════════════════════════════════════════════
# ALLOWED_HOSTS، CSRF، CORS، callbackِ زرین‌پال، برگشت از پرداخت و redirectِ بازار
# همه از این دو ساخته می‌شوند. پیش‌فرضشان همین دامنه‌های فعلی است.
# جابه‌جاییِ دامنه: docs/operations.md، بخشِ «عوض کردنِ دامنه».
#
# PUBLIC_BASE_URL=https://mathapi.tadrisino2.ir
# FRONTEND_URL=https://math.tadrisino2.ir
#
# فقط برای دامنه‌ی *اضافه* (مثلاً دامنه‌ی قبلی در دورانِ جابه‌جایی):
# DJANGO_ALLOWED_HOSTS=old.example.ir
# CSRF_TRUSTED_ORIGINS=https://old.example.ir
# CORS_ALLOWED_ORIGINS=https://old-app.example.ir
# تعدادِ پروکسی‌های جلوی اپ. سرورِ فعلی: ArvanCloud → nginx، یعنی 2.
# بیشتر از واقعیت = جعلِ IP و دور زدنِ محدودیتِ نرخ؛ کمتر = همه‌ی مهمان‌ها یک IP.
NUM_PROXIES=2
# ═══════════════════════════════════════════════════════════════════════
# ۳) پیامکِ کدِ ورود (کاوه‌نگار)
# ═══════════════════════════════════════════════════════════════════════
# بدونِ کلید، درخواستِ کد خطای صریحِ ۵۰۳ می‌دهد (نه موفقیتِ ساختگی).
KAVENEGAR_API_KEY=
# نامِ الگوی تأییدشده در پنلِ کاوه‌نگار (verify/lookup). روی سرورِ فعلی SMSVerify.
# خطِ این حساب پیامکِ متنِ آزاد را لغو می‌کند، پس بدونِ الگو کدی نمی‌رسد.
KAVENEGAR_OTP_TEMPLATE=
# خطِ اختصاصی؛ خالی = خطِ پیش‌فرضِ حساب.
KAVENEGAR_SENDER=
# شماره‌های تستیِ تیم با کدِ ثابت، بدونِ پیامک: «شماره:کد» با کاما.
# کد تصادفی باشد؛ پیش از کاربرِ واقعی خالی شود (/api/health تا آن‌وقت هشدار می‌دهد).
# TEST_LOGIN_PHONES=09120000001:834512,09120000002:110947
TEST_LOGIN_PHONES=
# ═══════════════════════════════════════════════════════════════════════
# ۴) دستیارِ هوش مصنوعی (RAG)
# ═══════════════════════════════════════════════════════════════════════
# بدونِ این دو کلید، POST /api/ai/ask پاسخِ آزمایشیِ ثابت (stub) می‌دهد.
# GapGPT پروکسیِ OpenAI است و فقط از داخلِ ایران جواب می‌دهد.
OPENAI_API_KEY=
OPENAI_BASE_URL=https://api.gapgpt.app/v1
QDRANT_URL=https://qdrant.tadrisino2.ir:443
QDRANT_API_KEY=
# مدل‌ها؛ پیش‌فرض‌ها در settings.py همین‌اند و سنجیده شده‌اند (docs/architecture.md).
# OPENAI_CHAT_MODEL=gpt-4.1-mini
# OPENAI_VISION_MODEL=gpt-4.1-mini
# مدلِ جدا فقط برای پایه‌های ۱۰ تا ۱۲؛ خالی = همان مدلِ بالا.
# OPENAI_CHAT_MODEL_UPPER=
# embedding را عوض نکنید مگر collectionهای Qdrant هم با همان مدل بازساخته شوند.
# OPENAI_EMBEDDING_MODEL=text-embedding-3-small
# سقفِ سوالِ روزانه از دستیار برای کاربرِ بدونِ اشتراک (پیش‌فرض ۳). سقفِ مشترکان روی
# هر پلن در ادمین است (ai_daily_quota). حسابِ هزینه: docs/ai-cost-and-guardrails.md
# AI_DAILY_QUOTA_FREE=2
# ═══════════════════════════════════════════════════════════════════════
# ۵) پرداخت
# ═══════════════════════════════════════════════════════════════════════
# زرین‌پال. بدونِ مرچنت، پرداخت ۵۰۳ می‌دهد. مرچنت باید برای دامنه‌ی PUBLIC_BASE_URL
# ثبت شده باشد، وگرنه زرین‌پال خطای -14 می‌دهد.
ZARINPAL_MERCHANT_ID=
# 1 = محیطِ آزمایشیِ زرین‌پال (بی‌پولِ واقعی). روی سرورِ فعلی خالی است: درگاهِ واقعی.
ZARINPAL_SANDBOX=
# کافه‌بازار (فقط خریدِ درون‌برنامه‌ایِ اپِ اندروید). روشِ ترجیحی توکنِ ثابتِ پیشخان
# است؛ سه متغیرِ OAuth فقط وقتی آن نیست. بدونِ هیچ‌کدام، مسیرِ بازار بسته می‌ماند.
BAZAAR_PISHKHAN_API_SECRET=
BAZAAR_CLIENT_ID=
BAZAAR_CLIENT_SECRET=
BAZAAR_REFRESH_TOKEN=
# BAZAAR_PACKAGE_NAME=ir.tadrisino.app
# ═══════════════════════════════════════════════════════════════════════
# ۶) اختیاری — پیش‌فرض‌ها معمولاً کافی‌اند
# ═══════════════════════════════════════════════════════════════════════
# کلیدِ مسیرهای داخلیِ سرور-به-سرور؛ خالی = آن مسیرها بسته‌اند (در DEBUG هم پیش‌فرضی ندارد).
# INTERNAL_SERVICE_KEY=
# مهمان‌های نمایشی که با «حذفِ حساب» پاک نمی‌شوند (با کاما). پیش‌فرض: frontend-test-device.
# DEMO_GUEST_DEVICE_IDS=
# با صفر قلب جلوی جواب دادن گرفته شود؟ پیش‌فرض روشن (از ۲۰۲۶-۱۰-۰۲)؛ ۰ خاموشش می‌کند.
# HEARTS_ENFORCED=1
# سقف‌های کدِ ورود: هر شماره در روز، و کلِ سامانه در روز (ضدِ SMS pumping).
# OTP_REQUEST_PHONE_DAILY_MAX=10
# OTP_SMS_GLOBAL_DAILY_MAX=300
# سقفِ خواندن روی اندپوینت‌های پرترافیک، و سقفِ /api/health.
# READ_THROTTLE_RATE=120/min
# HEALTH_THROTTLE_RATE=60/min
# سقفِ تمدیدِ توکن برای هر IP (همه‌ی کاربرانِ پشتِ یک وای‌فای یا CGNAT با هم).
# TOKEN_REFRESH_IP_RATE=600/hour
# سقفِ روزانه‌ی کلِ سامانه برای سوال‌هایی که به مدلِ هوش مصنوعی می‌رسند (ضدِ خرجِ بی‌سقف).
# AI_GLOBAL_DAILY_MAX=3000
# کشِ پاسخِ دستیار (ثانیه)؛ سوالِ تکراری دوباره به مدل نمی‌رود.
# RAG_ANSWER_CACHE_TTL=1800
# 1 = دستیار حتی با کلیدها هم پاسخِ آزمایشی بدهد.
# RAG_FORCE_MOCK=
# لاگ در فایل علاوه بر stdout (فقط داخلِ Docker).
# LOG_DIR=
# رمزِ ادمینِ Grafana در docker compose (فقط محلی).
# GRAFANA_ADMIN_PASSWORD=admin