ExecuteCampaignRealConcurrencyTests had been written off as a known flake for weeks — about 45% red on a full-suite run. It was not timing noise. It was reporting a real defect, and the defect is in production code. execute_campaign looped with .iterator(), which keeps the read cursor on the user table open for the whole loop — a loop that writes a CampaignDelivery per user and, on the sms branch, makes a network call to Kavenegar in between. On SQLite that open cursor locks the table against any concurrent writer, and the lock is not released until the loop ends. So _retry_on_sqlite_lock could never recover from it no matter how long it waited: widening its window from 0.5s to 2s (also done here, since the linear backoff was too tight regardless) moved the failure rate not at all on its own. Both branches now materialise the ids before the first write. After the change: 20 consecutive runs of the test itself and 3 full-suite runs, all green. This is not only a test fix. While we are on SQLite the same pattern produces the same lock in production, where it surfaces as "database is locked" on unrelated requests that happen to land during a campaign run. Also in this pass ----------------- - CLAUDE.md carried three sections each claiming to be the current state, with three different question counts (1,441 / 1,783 / reality 2,463). There is now one current-state section and the older two are labelled as history, so whoever picks this up next is not reading stale numbers as fact. Three other figures that had drifted are corrected against the database: the verdict split, the figure-gap share (881/~42% is now 508/~14% after the five books were re-read), and the node coverage. - Dropped four genuinely unused imports. The remaining pyflakes hits are the signal registrations in apps.py, which are imported for effect. - .env.example suggested gpt-4o for vision; gpt-4.1-mini measured both cheaper and more accurate and is what the server actually runs. Audited all 2,463 questions across every grade: no question with the wrong number of options, none with zero or two correct answers, no empty or duplicated option, no orphan lesson or node, no duplicate question inside a lesson. 458 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
120 lines
7.4 KiB
Plaintext
120 lines
7.4 KiB
Plaintext
# نمونهی فایلِ تنظیمات — هم برای ماشینِ خودتان، هم برای سرور.
|
||
#
|
||
# cp .env.example .env
|
||
#
|
||
# تقریباً هیچکدام اجباری نیستند. هر چیزی که از قبل قابلِ تصمیم بود در خودِ
|
||
# کد پیشفرض شده: دامنهها، origin های مجاز، و کلیدِ امضا (اولین اجرا خودش
|
||
# یک کلیدِ تصادفی میسازد و در .django_secret_key نگه میدارد). حتی با یک
|
||
# فایلِ خالی هم سرویس بالا میآید.
|
||
#
|
||
# روی سرور، بعد از یکبار ساختنِ این فایل، هر آپدیت فقط:
|
||
# git pull && ./deploy.sh
|
||
|
||
# -----------------------------------------------------------------------
|
||
# پایه
|
||
# -----------------------------------------------------------------------
|
||
# DEBUG اینجا عمداً 0 است.
|
||
#
|
||
# این فایل هم روی لپتاپِ توسعهدهنده کپی میشود هم روی سرور، و حالتِ پیشفرض
|
||
# باید آنی باشد که اگر کسی فراموشش کند، بیخطر بماند. یک بار همین فایل با
|
||
# مقدارِ 1 روی سرور نشست و ماهها آنجا ماند.
|
||
#
|
||
# ⚠️ DEBUG=1 روی سرورِ عمومی دو ریسک دارد:
|
||
# • صفحههای خطا کلِ تنظیمات و traceback را نشان میدهند
|
||
# • کدِ ورودِ آزمایشیِ 0000 برای هر شمارهای پذیرفته میشود، یعنی هر
|
||
# کسی میتواند بهجای هر کاربری وارد شود
|
||
#
|
||
# روی ماشینِ خودتان (و فقط آنجا) این را 1 کنید تا خطاها کامل دیده شوند.
|
||
DJANGO_DEBUG=0
|
||
|
||
# خالی بگذارید تا خودش ساخته و در .django_secret_key ذخیره شود.
|
||
DJANGO_SECRET_KEY=
|
||
|
||
# بدونِ Redis سرویس بالا میآید ولی محدودیتِ نرخ بینِ workerها مشترک نیست؛
|
||
# /api/health همین را هشدار میدهد. برای پروداکشنِ واقعی پرش کنید.
|
||
CACHE_URL=
|
||
|
||
# نامِ سرویسِ systemd تا deploy.sh خودش ریاستارت کند (فقط روی سرور).
|
||
# TADRISINO_SERVICE=
|
||
|
||
# -----------------------------------------------------------------------
|
||
# آدرسِ سرویس — این چهار تا پیشفرضِ درست دارند
|
||
# -----------------------------------------------------------------------
|
||
# تا پیش از این هر چهارتا باید دستی روی سرور اضافه میشدند، و چون در همین
|
||
# فایل نبودند هر بار از نو کشف میشدند: نبودِ اولی یعنی «Disallowed Host»،
|
||
# دومی یعنی ردِ ورود به پنلِ ادمین، سومی یعنی بلاک شدنِ فرانت در مرورگر، و
|
||
# چهارمی یعنی شکستنِ بیسروصدای مسیرِ پرداخت.
|
||
#
|
||
# حالا هر چهارتا مقدارِ پیشفرضِ دامنهی خودمان را دارند و لازم نیست ست شوند.
|
||
# فقط اگر دامنه عوض شد، اینها را باز کنید:
|
||
#
|
||
# DJANGO_ALLOWED_HOSTS=api.math.tadrisino2.ir,math.tadrisino2.ir,localhost,127.0.0.1
|
||
# CSRF_TRUSTED_ORIGINS=https://api.math.tadrisino2.ir
|
||
# CORS_ALLOWED_ORIGINS=https://math.tadrisino2.ir
|
||
# PUBLIC_BASE_URL=https://api.math.tadrisino2.ir
|
||
|
||
# اختیاری — بدونش OTP واقعی ارسال نمیشه (فقط خطای صریح ۵۰۳ برمیگرده،
|
||
# طبق رفتارِ موجودِ accounts/services.py)
|
||
KAVENEGAR_API_KEY=
|
||
|
||
# رمزِ ادمینِ Grafana (کاربرِ admin) — فقط برای دسترسیِ محلی
|
||
GRAFANA_ADMIN_PASSWORD=admin
|
||
|
||
# -----------------------------------------------------------------------
|
||
# RAG / AI tutor (اپ rag) — موقعِ تست پر کنید
|
||
# -----------------------------------------------------------------------
|
||
# بدونِ OPENAI_API_KEY و QDRANT_API_KEY، مسیرِ RAG غیرفعاله و
|
||
# POST /api/ai/ask همون پاسخِ stubِ قبلی رو میده (سازگاریِ عقبرو).
|
||
# فعلاً از GapGPT (پروکسیِ OpenAI برای ایران) استفاده میکنیم.
|
||
OPENAI_API_KEY=
|
||
OPENAI_BASE_URL=https://api.gapgpt.app/v1
|
||
# اگه بعداً خواستی مستقیم OpenAI واقعی رو تست کنی: کلیدِ واقعی رو این پایین
|
||
# پارک کن، بعد فقط دو خطِ بالا رو باهاش عوض کن. هیچ منطقِ سوییچی در کد نیست
|
||
# تا ساده و بیاشتباه بمونه.
|
||
# OPENAI_BASE_URL=https://api.openai.com/v1
|
||
OPENAI_API_KEY_OPENAI_DIRECT=
|
||
OPENAI_CHAT_MODEL=gpt-4o-mini
|
||
OPENAI_VISION_MODEL=gpt-4.1-mini
|
||
# این رو تغییر نده؛ باید با مدلی که تیمِ دیگه باهاش collectionها رو ساخته یکی باشه.
|
||
OPENAI_EMBEDDING_MODEL=text-embedding-3-small
|
||
|
||
QDRANT_URL=https://qdrant.tadrisino2.ir:443
|
||
QDRANT_API_KEY=
|
||
|
||
# نامِ collection = f"{subject} {grade}".lower() . collectionهای موجود روی
|
||
# سرورِ تیمِ دیگه (بررسیشده): «علوم تجربی سوم» ، «مطالعات اجتماعی چهارم» ،
|
||
# «مطالعات اجتماعی ششم» ، «ریاضی سوم» ، «ریاضی ششم» ، «ریاضی هفتم».
|
||
# یعنی جزءِ گرید فقط «سوم/چهارم/ششم/هفتم» است (بدونِ «دبستان»).
|
||
#
|
||
# کلیدِ فیلترِ درس دستی تنظیم نمیشه — rag/services/known_collections.py خودش
|
||
# تشخیص میده: ریاضی → `chapter` (و شمارهی فصل رو به رشتهی دقیق تبدیل میکنه)،
|
||
# بقیه → `lesson_number`.
|
||
RAG_COLLECTION_NAME_TEMPLATE={subject} {grade}
|
||
RAG_RETRIEVAL_TOP_K=4
|
||
# RAG_FORCE_MOCK=1 یعنی حتی با وجودِ کلیدها هم مسیرِ RAG خاموش بمونه.
|
||
RAG_FORCE_MOCK=
|
||
|
||
# -----------------------------------------------------------------------
|
||
# زرینپال (اپ payment) — درگاهِ اصلیِ پرداختِ ما
|
||
# -----------------------------------------------------------------------
|
||
# بدونِ ZARINPAL_MERCHANT_ID، مسیرِ پرداخت fail-closed میمونه (خطای صریحِ
|
||
# ۵۰۳ بهجایِ پرداختِ ساختگیِ موفق).
|
||
# PUBLIC_BASE_URL بالاتر توضیح داده شده؛ پیشفرض دارد.
|
||
ZARINPAL_MERCHANT_ID=
|
||
# 1 = محیطِ sandbox زرینپال (برای تست بدونِ پرداختِ واقعی)
|
||
ZARINPAL_SANDBOX=1
|
||
# اختیاری — بعدِ تاییدِ پرداخت، کاربر به این URL (مثلاً یک deep link اپ) با
|
||
# ?orderId=...&status=... ریدایرکت میشه. خالی = پاسخِ JSON ساده.
|
||
ZARINPAL_REDIRECT_URL=
|
||
|
||
# -----------------------------------------------------------------------
|
||
# تنظیماتِ مقیاسپذیری (همه دیفالتِ امن دارن)
|
||
# -----------------------------------------------------------------------
|
||
# persistent DB connection (ثانیه). با SQLite بیاثر؛ بعد از Postgres مهم.
|
||
DB_CONN_MAX_AGE=60
|
||
# کشِ کوتاهِ ردیفِ کاربر در auth تا هر request یک SELECT نزنه. 0 = خاموش
|
||
# (پیشفرض). توصیهی پروداکشن با Redis: 30. با هر save/delete روی User باطل میشه.
|
||
AUTH_USER_CACHE_TTL=0
|
||
# سقفِ ارزانِ خواندن روی endpointهای پرترافیکِ GET (home/leaderboard/summary/...).
|
||
READ_THROTTLE_RATE=120/min
|