Do not connect to the shared Redis: CACHE_URL stays empty, own Redis is an open decision

The owner decided (2026-10-07) that we do not connect to anything other teams run on the box, Redis
included. CACHE_URL was emptied on the server and our keys were removed from the shared Redis.

- settings: drop KEY_PREFIX / CACHE_KEY_PREFIX (it only existed because the Redis was shared); comments
  say CACHE_URL is intentionally empty and ResilientRedisCache is ready for our own Redis.
- config/cache.py docstring and tests updated accordingly (11 tests, still run against a local redis-server).
- CLAUDE.md: new server rule 8 (touch only our own files and service), status block, historical line
  that said "db1 is free" now says not to connect. README, operations, handbook, security doc (item 9
  reopened as an open decision) say the same.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 14:41:39 +03:30
parent c129f64180
commit d584d62904
8 changed files with 59 additions and 42 deletions

View File

@@ -1,10 +1,14 @@
"""
RedisCache که با قطعی‌ی Redis درخواست‌ها را خراب نمی‌کند، ولی بی‌صدا هم نیست.
چرا: از ۲۰۲۶-۱۰-۰۷ سقفِ نرخ، کشِ برنامه‌ی درسی، کشِ جوابِ دستیار و شمارنده‌های ورود روی Redisِ مشترکِ
ماشین‌اند. `django.core.cache.backends.redis.RedisCache` با هر خطای شبکه استثنا می‌دهد و چون
DRF برای هر درخواستِ throttleدار کش را می‌خواند، قطعیِ Redis یعنی ۵۰۰ برای تقریباً همه‌ی API.
Redis مالِ ما نیست و ما کنترلی روی بالا بودنش نداریم.
وضعیت: فقط وقتی فعال است که `CACHE_URL` ست باشد. روی سرور (۲۰۲۶-۱۰-۰۷) ست نیست و کش `LocMemCache`
است: Redisِ آن ماشین مالِ تیم‌های دیگر است و عمداً به آن وصل نمی‌شویم. این کلاس برای Redisِ اختصاصیِ
خودمان نوشته و آزموده شده است.
چرا: سقفِ نرخ، کشِ برنامه‌ی درسی، کشِ جوابِ دستیار و شمارنده‌های ورود از کش می‌خوانند.
`django.core.cache.backends.redis.RedisCache` با هر خطای شبکه استثنا می‌دهد و چون DRF برای هر
درخواستِ throttleدار کش را می‌خواند، قطعیِ Redis یعنی ۵۰۰ برای تقریباً همه‌ی API (هر هفت تستِ
`config/tests_cache_resilience.py` روی کشِ معمولی می‌شکند).
رفتار هنگامِ خطا (همه با `logger.error`، حداکثر هر ۳۰ ثانیه یک بار برای هر پروسه تا لاگ پر نشود؛
و `/api/health` در همان حال ۵۰۳ می‌دهد چون پروبِ کش شکست می‌خورد):

View File

@@ -353,12 +353,11 @@ CACHE_URL = os.environ.get('CACHE_URL', '')
if CACHE_URL:
CACHES = {
'default': {
# ResilientRedisCache: قطعیِ Redisِ مشترکِ ماشین API را ۵۰۰ نمی‌کند (config/cache.py شرح می‌دهد
# چه چیزی در عوضش اعمال نمی‌شود). KEY_PREFIX چون Redis بینِ چند اپ مشترک است و کلیدِ ما با
# هیچ کلیدِ دیگری قاطی نشود؛ مقدارش یک انتخابِ نام‌گذاری است و عوض‌کردنش فقط کشِ فعلی را خالی می‌کند.
# ResilientRedisCache: قطعیِ Redis API را ۵۰۰ نمی‌کند (config/cache.py شرح می‌دهد چه چیزی در
# عوضش اعمال نمی‌شود). روی سرور فعلاً CACHE_URL خالی است: Redisِ روی آن ماشین مالِ تیم‌های
# دیگر است و عمداً به آن وصل نمی‌شویم؛ این مسیر برای Redisِ اختصاصیِ خودمان آماده است.
'BACKEND': 'config.cache.ResilientRedisCache',
'LOCATION': CACHE_URL,
'KEY_PREFIX': os.environ.get('CACHE_KEY_PREFIX', 'tadrisino-mathapp'),
# بدونِ timeout، Redisِ گیرکرده هر درخواست را تا پایانِ timeoutِ gunicorn (۱۲۰ ثانیه) نگه می‌دارد.
'OPTIONS': {'socket_connect_timeout': 1, 'socket_timeout': 1},
}

View File

@@ -24,12 +24,9 @@ from . import cache as cache_module
BACKEND = 'config.cache.ResilientRedisCache'
def _caches(location, prefix=None):
conf = {'BACKEND': BACKEND, 'LOCATION': location,
'OPTIONS': {'socket_connect_timeout': 1, 'socket_timeout': 1}}
if prefix:
conf['KEY_PREFIX'] = prefix
return {'default': conf}
def _caches(location):
return {'default': {'BACKEND': BACKEND, 'LOCATION': location,
'OPTIONS': {'socket_connect_timeout': 1, 'socket_timeout': 1}}}
class _Quiet:
@@ -141,9 +138,9 @@ class RealRedisTests(_Quiet, TestCase):
self.proc.terminate()
self.proc.wait(timeout=5)
def test_normal_behaviour_and_key_prefix(self):
def test_normal_behaviour_and_keys_have_a_ttl(self):
import redis
with override_settings(CACHES=_caches(f'redis://127.0.0.1:{self.port}/1', prefix='tadrisino-test')):
with override_settings(CACHES=_caches(f'redis://127.0.0.1:{self.port}/1')):
c = caches['default']
c.set('k', {'a': 1}, 30)
self.assertEqual(c.get('k'), {'a': 1})
@@ -153,17 +150,16 @@ class RealRedisTests(_Quiet, TestCase):
with self.assertRaises(ValueError):
c.incr('missing')
raw = redis.Redis(host='127.0.0.1', port=self.port, db=1)
self.assertTrue(all(b'tadrisino-test' in k for k in raw.keys('*')))
self.assertEqual(redis.Redis(host='127.0.0.1', port=self.port, db=0).dbsize(), 0) # دیتابیسِ دیگران دست نخورد
self.assertEqual(redis.Redis(host='127.0.0.1', port=self.port, db=0).dbsize(), 0) # فقط دیتابیسِ خودمان
self.assertGreater(raw.ttl(raw.keys('*k')[0]), 0) # TTL گذاشته شده؛ کلیدِ بی‌انقضا نمی‌ماند
def test_throttle_is_shared_and_health_is_ok(self):
with override_settings(CACHES=_caches(f'redis://127.0.0.1:{self.port}/1', prefix='t')):
with override_settings(CACHES=_caches(f'redis://127.0.0.1:{self.port}/1')):
response = APIClient().get('/api/health')
self.assertIs(response.json()['checks']['cache']['ok'], True)
def test_redis_dying_mid_flight_and_coming_back_without_restarting_the_app(self):
with override_settings(CACHES=_caches(f'redis://127.0.0.1:{self.port}/1', prefix='t')):
with override_settings(CACHES=_caches(f'redis://127.0.0.1:{self.port}/1')):
c = caches['default']
c.set('k', 'before', 60)
self.assertEqual(c.get('k'), 'before')