Files
MathApp/config/tests_offsite_backup.py
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

44 lines
2.1 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
"""
وضعیتِ کپیِ زنده‌ی دیتابیس در /api/health (config/health.py: _slot_state، _offsite_warnings).
پرسشِ واقعی از pg_replication_slots این‌جا اجرا نمی‌شود (تست‌ها روی SQLite‌اند)؛ روی سرور با اسلاتِ
واقعی دیده شد (docs/operations.md «بکاپ»).
"""
from django.test import SimpleTestCase, override_settings
from config import health
class SlotStateTests(SimpleTestCase):
def test_missing_slot(self):
self.assertEqual(health._slot_state(None)['state'], 'slot_missing')
def test_streaming_and_caught_up_is_ok(self):
self.assertEqual(health._slot_state((True, 'reserved', 0)), {'state': 'ok', 'lagBytes': 0})
def test_inactive_slot_is_disconnected(self):
self.assertEqual(health._slot_state((False, 'reserved', 0))['state'], 'disconnected')
def test_lost_slot_wins_over_everything(self):
self.assertEqual(health._slot_state((False, 'lost', None))['state'], 'lost')
@override_settings(OFFSITE_WAL_MAX_LAG_BYTES=100)
def test_lag_above_limit(self):
self.assertEqual(health._slot_state((True, 'reserved', 101))['state'], 'lagging')
self.assertEqual(health._slot_state((True, 'reserved', 100))['state'], 'ok')
class OffsiteWarningTests(SimpleTestCase):
def test_ok_and_dev_are_silent(self):
self.assertEqual(health._offsite_warnings({'state': 'ok', 'lagBytes': 0}), [])
self.assertEqual(health._offsite_warnings({'state': 'not_postgres'}), [])
def test_every_bad_state_warns(self):
for state in ('not_configured', 'slot_missing', 'disconnected', 'lagging', 'lost', 'error'):
self.assertEqual(len(health._offsite_warnings({'state': state})), 1, state)
def test_sqlite_reports_not_postgres(self):
# سوئیت روی Postgres هم اجرا می‌شود (پیش از هر تغییرِ دیتابیسی)؛ آن‌جا این حالت معنی ندارد.
if health.connection.vendor != 'sqlite':
self.skipTest('فقط روی SQLite')
self.assertEqual(health._offsite_backup(), {'state': 'not_postgres'})