refactor(redis): strengthen shared cache invalidation

This commit is contained in:
3252a8
2026-05-21 06:26:48 +03:00
parent 3405d12696
commit 044cb4de7e
10 changed files with 237 additions and 61 deletions
+5 -1
View File
@@ -1,6 +1,7 @@
# ruff: noqa: F401,F403,F405,I001
from ._runtime import * # noqa: F403,F405
from .auth import _hash_email_password
from .common import _invalidate_webapp_user_caches
async def account_email_request_route(request: web.Request) -> web.Response:
@@ -174,6 +175,7 @@ async def account_email_verify_route(request: web.Request) -> web.Response:
logger.exception("Email account link failed")
return _json_error(500, "link_failed", "Link failed")
await _invalidate_webapp_user_caches(settings, user_id, final_user_id, include_devices=True)
if should_notify_email_linked:
try:
from bot.services.notification_service import NotificationService
@@ -276,7 +278,7 @@ async def account_password_confirm_route(request: web.Request) -> web.Response:
logger.exception("Email password setup failed")
return _json_error(500, "password_setup_failed", "Password setup failed")
await cache_delete(settings, redis_key(settings, "cache", "webapp", "me", user_id))
await _invalidate_webapp_user_caches(settings, user_id)
return web.json_response({"ok": True, "password_auth_enabled": True})
@@ -396,6 +398,7 @@ async def account_telegram_link_route(request: web.Request) -> web.Response:
logger.exception("Telegram account link failed")
return _json_error(500, "link_failed", "Link failed")
await _invalidate_webapp_user_caches(settings, user_id, final_user_id, include_devices=True)
if should_notify_telegram_linked and final_telegram_id:
try:
from bot.services.notification_service import NotificationService
@@ -488,6 +491,7 @@ async def account_language_route(request: web.Request) -> web.Response:
await session.flush()
await session.commit()
await _invalidate_webapp_user_caches(settings, user_id)
return web.json_response({"ok": True, "language": language})
+24 -19
View File
@@ -820,25 +820,30 @@ async def _enforce_webapp_rate_limit(
or "unknown"
)
key = f"{action}:{ip_address}:{int(user_id)}"
redis = await get_redis(settings)
if redis is not None:
redis_rate_key = redis_key(settings, "rate-limit", "webapp", key)
current = await redis.incr(redis_rate_key)
if current == 1:
await redis.expire(redis_rate_key, settings.WEBAPP_RATE_LIMIT_TTL_SECONDS)
if current > settings.WEBAPP_RATE_LIMIT_MAX_REQUESTS:
ttl = await redis.ttl(redis_rate_key)
retry_after = max(1, int(ttl if ttl and ttl > 0 else WEBAPP_RATE_LIMIT_WINDOW_SECONDS))
return web.json_response(
{
"ok": False,
"error": "rate_limited",
"retry_after": retry_after,
},
status=429,
headers={"Retry-After": str(retry_after)},
)
return None
try:
redis = await get_redis(settings)
if redis is not None:
redis_rate_key = redis_key(settings, "rate-limit", "webapp", key)
current = await redis.incr(redis_rate_key)
if current == 1:
await redis.expire(redis_rate_key, settings.WEBAPP_RATE_LIMIT_TTL_SECONDS)
if current > settings.WEBAPP_RATE_LIMIT_MAX_REQUESTS:
ttl = await redis.ttl(redis_rate_key)
retry_after = max(
1, int(ttl if ttl and ttl > 0 else WEBAPP_RATE_LIMIT_WINDOW_SECONDS)
)
return web.json_response(
{
"ok": False,
"error": "rate_limited",
"retry_after": retry_after,
},
status=429,
headers={"Retry-After": str(retry_after)},
)
return None
except Exception as exc:
logger.warning("Redis webapp rate limiter unavailable; using local fallback: %s", exc)
buckets: Dict[str, deque[float]] = request.app["webapp_rate_limit_buckets"]
lock: asyncio.Lock = request.app["webapp_rate_limit_lock"]
+5
View File
@@ -1,5 +1,6 @@
# ruff: noqa: F401,F403,F405,I001
from ._runtime import * # noqa: F403,F405
from .common import _invalidate_webapp_user_caches
def _resolve_telegram_bot_id(bot_token: str) -> Optional[int]:
@@ -386,6 +387,7 @@ async def telegram_oauth_callback_route(request: web.Request) -> web.Response:
logger.exception("Telegram OAuth callback failed")
raise redirect(redirect_path, "failed")
await _invalidate_webapp_user_caches(settings, final_user_id, include_devices=True)
token = create_webapp_session_token(settings, int(final_user_id))
response = web.HTTPFound(_telegram_oauth_redirect_url(redirect_path, status="success"))
_clear_telegram_oauth_state_cookie(response)
@@ -480,6 +482,7 @@ async def auth_token_route(request: web.Request) -> web.Response:
logger.exception("WebApp auth failed")
return _json_error(500, "auth_failed", "Auth failed")
await _invalidate_webapp_user_caches(settings, authenticated_user_id, include_devices=True)
token = create_webapp_session_token(settings, int(authenticated_user_id))
return _build_webapp_auth_response(settings, {"ok": True}, token=token)
@@ -694,6 +697,7 @@ async def email_auth_verify_route(request: web.Request) -> web.Response:
logger.exception("Email WebApp auth failed")
return _json_error(500, "auth_failed", "Auth failed")
await _invalidate_webapp_user_caches(settings, int(db_user.user_id), include_devices=True)
if created_user:
try:
from bot.services.notification_service import NotificationService
@@ -801,6 +805,7 @@ async def email_auth_magic_route(request: web.Request) -> web.Response:
logger.exception("Email magic-link auth failed")
return _json_error(500, "auth_failed", "Auth failed")
await _invalidate_webapp_user_caches(settings, int(db_user.user_id), include_devices=True)
if created_user and verified_email:
try:
from bot.services.notification_service import NotificationService
+24
View File
@@ -17,6 +17,30 @@ def _json_error(status: int, code: str, message: str) -> web.Response:
)
async def _invalidate_webapp_user_caches(
settings: Settings,
*user_ids: Optional[int],
include_devices: bool = False,
) -> None:
keys: List[str] = []
seen: set[int] = set()
for raw_user_id in user_ids:
if raw_user_id is None:
continue
try:
user_id = int(raw_user_id)
except (TypeError, ValueError):
continue
if user_id in seen:
continue
seen.add(user_id)
keys.append(redis_key(settings, "cache", "webapp", "me", user_id))
if include_devices:
keys.append(redis_key(settings, "cache", "webapp", "devices", user_id))
if keys:
await cache_delete(settings, *keys)
def _validation_error_response(exc: ValidationError) -> web.Response:
for error in exc.errors():
loc = error.get("loc") or ()