perf: reduce webapp and admin cache stampedes

This commit is contained in:
3252a8
2026-05-21 09:44:50 +03:00
parent ccb125ab9d
commit f2c722bdfc
9 changed files with 423 additions and 78 deletions
@@ -0,0 +1,81 @@
from __future__ import annotations
from typing import Any, Awaitable, Callable, Optional
from bot.infra.redis import cache_delete, redis_key
from bot.utils.ttl_cache import AsyncTTLCache
from config.settings import Settings
_WEBAPP_USER_PAYLOAD_CACHES: dict[tuple[int, str, int], AsyncTTLCache] = {}
def _webapp_user_payload_cache(
settings: Settings,
namespace: str,
ttl_seconds: int,
) -> Optional[AsyncTTLCache]:
ttl = max(0, int(ttl_seconds or 0))
if ttl <= 0:
return None
cache_key = (id(settings), namespace, ttl)
cache = _WEBAPP_USER_PAYLOAD_CACHES.get(cache_key)
if cache is None:
cache = AsyncTTLCache(
ttl_seconds=ttl,
settings=settings,
namespace=f"webapp:{namespace}",
)
_WEBAPP_USER_PAYLOAD_CACHES[cache_key] = cache
return cache
async def webapp_cached_user_payload(
settings: Settings,
namespace: str,
user_id: int,
ttl_seconds: int,
loader: Callable[[], Awaitable[Any]],
) -> Any:
cache = _webapp_user_payload_cache(settings, namespace, ttl_seconds)
if cache is None:
return await loader()
return await cache.get_or_load(str(int(user_id)), loader)
def invalidate_local_webapp_user_payload(
settings: Settings,
namespace: str,
user_id: int,
) -> None:
key = str(int(user_id))
for (settings_id, cache_namespace, _ttl), cache in tuple(
_WEBAPP_USER_PAYLOAD_CACHES.items()
):
if settings_id == id(settings) and cache_namespace == namespace:
cache.invalidate(key)
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))
invalidate_local_webapp_user_payload(settings, "me", user_id)
if include_devices:
keys.append(redis_key(settings, "cache", "webapp", "devices", user_id))
invalidate_local_webapp_user_payload(settings, "devices", user_id)
if keys:
await cache_delete(settings, *keys)