Redis outage must not 500 the API: ResilientRedisCache, key prefix, 1s socket timeouts

Since 2026-10-07 the cache is the shared Redis on the box. Django's RedisCache raises on any network
error and DRF reads the cache for every throttled request, so a Redis outage meant 500 on almost every
route, /admin/login/ and OTP request included (all seven drill tests fail on the stock backend).

config.cache.ResilientRedisCache turns Redis errors into logged (rate-limited ERROR) misses:
get->default, set/delete->ignored, add->True, incr->ValueError. Cache-based caps (DRF throttles, hourly
OTP window, admin-login limiter, payment reconcile lock) are NOT enforced during an outage (fail-open);
DB-backed caps stay. /api/health returns 503 meanwhile. KEY_PREFIX namespaces our keys on the shared
Redis; 1s connect/read timeouts stop a hung Redis from holding requests for gunicorn's 120s.

Tested: 11 tests, incl. a real local redis-server killed mid-test and restarted without restarting the
app. Not tested yet on the server at the time of this commit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 14:16:01 +03:30
parent 7929f6bd85
commit 277927046d
3 changed files with 312 additions and 1 deletions

View File

@@ -353,8 +353,14 @@ CACHE_URL = os.environ.get('CACHE_URL', '')
if CACHE_URL:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
# ResilientRedisCache: قطعیِ Redisِ مشترکِ ماشین API را ۵۰۰ نمی‌کند (config/cache.py شرح می‌دهد
# چه چیزی در عوضش اعمال نمی‌شود). KEY_PREFIX چون 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},
}
}
elif DEBUG: