feat: add response cache middleware with poisoning-safe keys
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
"""进程内缓存后端: dict + 过期时刻;测试与无 Redis 场景使用。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
|
||||
class InMemoryCache:
|
||||
"""笨 KV(CacheBackend 契约);容量不设限——库内默认 TTL 必填防无界增长。"""
|
||||
|
||||
def __init__(self, now: Callable[[], float] = time.monotonic) -> None:
|
||||
self._now = now
|
||||
self._store: dict[str, tuple[float, str]] = {}
|
||||
|
||||
async def get(self, key: str) -> str | None:
|
||||
item = self._store.get(key)
|
||||
if item is None:
|
||||
return None
|
||||
expires_at, value = item
|
||||
if self._now() >= expires_at:
|
||||
del self._store[key]
|
||||
return None
|
||||
return value
|
||||
|
||||
async def set(self, key: str, value: str, ttl_s: int) -> None:
|
||||
self._store[key] = (self._now() + ttl_s, value)
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Redis 缓存后端: 笨 KV(CacheBackend 契约);redis 是 optional extra。
|
||||
|
||||
降级不在这里做——本类忠实上抛异常,静默降级是 CacheMW 的职责(算法一份)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
import redis.asyncio as aioredis
|
||||
except ImportError as _exc: # pragma: no cover - 依赖缺失路径
|
||||
aioredis = None
|
||||
_IMPORT_ERROR = _exc
|
||||
else:
|
||||
_IMPORT_ERROR = None
|
||||
|
||||
|
||||
class RedisCache:
|
||||
"""基于 redis.asyncio 的 KV;value 为 JSON 字符串,TTL 由调用方传入。"""
|
||||
|
||||
def __init__(self, client: Any) -> None:
|
||||
if aioredis is None:
|
||||
raise ImportError(
|
||||
"Redis 缓存后端需要 redis 包: pip install 'polygateway[redis]'"
|
||||
) from _IMPORT_ERROR
|
||||
self._client = client
|
||||
|
||||
@classmethod
|
||||
def from_url(cls, url: str) -> RedisCache:
|
||||
if aioredis is None:
|
||||
raise ImportError(
|
||||
"Redis 缓存后端需要 redis 包: pip install 'polygateway[redis]'"
|
||||
) from _IMPORT_ERROR
|
||||
return cls(aioredis.from_url(url, decode_responses=True))
|
||||
|
||||
async def get(self, key: str) -> str | None:
|
||||
return await self._client.get(key)
|
||||
|
||||
async def set(self, key: str, value: str, ttl_s: int) -> None:
|
||||
await self._client.set(key, value, ex=ttl_s)
|
||||
|
||||
async def aclose(self) -> None:
|
||||
await self._client.aclose()
|
||||
Reference in New Issue
Block a user