mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-03 22:04:57 +08:00
fix(types): annotate cache proxy contracts
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
"""
|
||||
进程缓存契约、内存后端和同步异步缓存装饰器。
|
||||
"""
|
||||
|
||||
import contextvars
|
||||
import inspect
|
||||
import logging
|
||||
@@ -6,7 +10,7 @@ from abc import ABC, abstractmethod
|
||||
from contextlib import asynccontextmanager, contextmanager
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
from typing import Any, AsyncGenerator, Callable, Dict, Generator, Literal, Optional, Tuple, Union
|
||||
from typing import Any, AsyncGenerator, Callable, Dict, Generator, Literal, Optional, Tuple, TypedDict, Union, Unpack, cast
|
||||
|
||||
from cachetools import LRUCache as MemoryLRUCache
|
||||
from cachetools import TLRUCache as MemoryTLRUCache
|
||||
@@ -43,7 +47,8 @@ def configure_cache_factories(
|
||||
async_redis_factory: Optional[Callable[[Optional[int]], "AsyncCacheBackend"]] = None,
|
||||
redis_probe: Optional[Callable[[], bool]] = None,
|
||||
) -> None:
|
||||
"""注入配置读取器和具体缓存适配器工厂。
|
||||
"""
|
||||
注入配置读取器和具体缓存适配器工厂。
|
||||
|
||||
Redis 工厂与探测器只在启动层选用 Redis 缓存时传入;此时 ``backend_type_provider``
|
||||
才可能返回 ``redis``,未接入 Redis 的进程不会走到 Redis 分支。
|
||||
@@ -60,7 +65,9 @@ def configure_cache_factories(
|
||||
|
||||
|
||||
def probe_redis_cache() -> Optional[bool]:
|
||||
"""探测启动时接入的 Redis 缓存是否可连接;本进程未接入 Redis 时返回 None。"""
|
||||
"""
|
||||
探测启动时接入的 Redis 缓存是否可连接;本进程未接入 Redis 时返回 None。
|
||||
"""
|
||||
if _redis_probe is None:
|
||||
return None
|
||||
return _redis_probe()
|
||||
@@ -70,6 +77,28 @@ def probe_redis_cache() -> Optional[bool]:
|
||||
_fresh = contextvars.ContextVar('fresh', default=False)
|
||||
|
||||
|
||||
class _RegionOptions(TypedDict, total=False):
|
||||
"""
|
||||
缓存代理传给后端的区域选项。
|
||||
|
||||
:ivar region: 显式覆盖代理绑定区域;None 保留后端默认区域语义
|
||||
"""
|
||||
|
||||
region: Optional[str]
|
||||
|
||||
|
||||
class _WriteOptions(_RegionOptions, total=False):
|
||||
"""
|
||||
缓存写入及批量写入选项。
|
||||
|
||||
:ivar ttl: 当前条目的有效期,单位秒;None 沿用后端默认值
|
||||
:ivar maxsize: 内存区域首次创建时的最大容量;None 沿用默认容量
|
||||
"""
|
||||
|
||||
ttl: Optional[int]
|
||||
maxsize: Optional[int]
|
||||
|
||||
|
||||
class CacheBackend(ABC):
|
||||
"""
|
||||
缓存后端基类,定义通用的缓存接口
|
||||
@@ -104,7 +133,7 @@ class CacheBackend(ABC):
|
||||
"""
|
||||
return self.exists(key)
|
||||
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> Generator[str, None, None]:
|
||||
"""
|
||||
返回缓存的迭代器,类似 iter(dict)
|
||||
"""
|
||||
@@ -119,7 +148,7 @@ class CacheBackend(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def set(self, key: str, value: Any, ttl: Optional[int] = None,
|
||||
region: Optional[str] = DEFAULT_CACHE_REGION, **kwargs) -> None:
|
||||
region: Optional[str] = DEFAULT_CACHE_REGION, **kwargs: object) -> None:
|
||||
"""
|
||||
设置缓存
|
||||
|
||||
@@ -197,7 +226,7 @@ class CacheBackend(ABC):
|
||||
yield value
|
||||
|
||||
def update(self, other: Dict[str, Any], region: Optional[str] = DEFAULT_CACHE_REGION,
|
||||
ttl: Optional[int] = None, **kwargs) -> None:
|
||||
ttl: Optional[int] = None, **kwargs: object) -> None:
|
||||
"""
|
||||
更新缓存,类似 dict.update()
|
||||
"""
|
||||
@@ -228,7 +257,7 @@ class CacheBackend(ABC):
|
||||
return key, value
|
||||
|
||||
def setdefault(self, key: str, default: Any = None, region: Optional[str] = DEFAULT_CACHE_REGION,
|
||||
ttl: Optional[int] = None, **kwargs) -> Any:
|
||||
ttl: Optional[int] = None, **kwargs: object) -> Any:
|
||||
"""
|
||||
设置默认值,类似 dict.setdefault()
|
||||
"""
|
||||
@@ -261,7 +290,9 @@ class CacheBackend(ABC):
|
||||
|
||||
|
||||
class AtomicCacheBackend(CacheBackend):
|
||||
"""支持严格写入和原子领取的一次性缓存后端契约。"""
|
||||
"""
|
||||
支持严格写入和原子领取的一次性缓存后端契约。
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def store(
|
||||
@@ -272,7 +303,9 @@ class AtomicCacheBackend(CacheBackend):
|
||||
region: Optional[str] = DEFAULT_CACHE_REGION,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""写入缓存;后端故障必须向调用方传播。"""
|
||||
"""
|
||||
写入缓存;后端故障必须向调用方传播。
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def consume(
|
||||
@@ -280,7 +313,9 @@ class AtomicCacheBackend(CacheBackend):
|
||||
key: str,
|
||||
region: Optional[str] = DEFAULT_CACHE_REGION,
|
||||
) -> Any:
|
||||
"""原子读取并删除缓存值,不存在时返回 None。"""
|
||||
"""
|
||||
原子读取并删除缓存值,不存在时返回 None。
|
||||
"""
|
||||
|
||||
def pop(
|
||||
self,
|
||||
@@ -288,7 +323,9 @@ class AtomicCacheBackend(CacheBackend):
|
||||
default: Any = None,
|
||||
region: Optional[str] = DEFAULT_CACHE_REGION,
|
||||
) -> Any:
|
||||
"""以原子领取实现兼容的字典 pop 语义。"""
|
||||
"""
|
||||
以原子领取实现兼容的字典 pop 语义。
|
||||
"""
|
||||
value = self.consume(key=key, region=region)
|
||||
if value is not None:
|
||||
return value
|
||||
@@ -304,7 +341,7 @@ class AsyncCacheBackend(CacheBackend):
|
||||
|
||||
@abstractmethod
|
||||
async def set(self, key: str, value: Any, ttl: Optional[int] = None,
|
||||
region: Optional[str] = DEFAULT_CACHE_REGION, **kwargs) -> None:
|
||||
region: Optional[str] = DEFAULT_CACHE_REGION, **kwargs: object) -> None:
|
||||
"""
|
||||
设置缓存
|
||||
|
||||
@@ -360,10 +397,10 @@ class AsyncCacheBackend(CacheBackend):
|
||||
@abstractmethod
|
||||
async def items(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> AsyncGenerator[Tuple[str, Any], None]:
|
||||
"""
|
||||
获取指定区域的所有缓存项
|
||||
获取指定区域的所有缓存项。
|
||||
|
||||
:param region: 缓存的区
|
||||
:return: 返回一个字典,包含所有缓存键值对
|
||||
:param region: 缓存的区域
|
||||
:return: 可直接用于 async for 的异步迭代器
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -382,7 +419,7 @@ class AsyncCacheBackend(CacheBackend):
|
||||
yield value
|
||||
|
||||
async def update(self, other: Dict[str, Any], region: Optional[str] = DEFAULT_CACHE_REGION,
|
||||
ttl: Optional[int] = None, **kwargs) -> None:
|
||||
ttl: Optional[int] = None, **kwargs: object) -> None:
|
||||
"""
|
||||
更新缓存,类似 dict.update()(异步)
|
||||
"""
|
||||
@@ -415,7 +452,7 @@ class AsyncCacheBackend(CacheBackend):
|
||||
return key, value
|
||||
|
||||
async def setdefault(self, key: str, default: Any = None, region: Optional[str] = DEFAULT_CACHE_REGION,
|
||||
ttl: Optional[int] = None, **kwargs) -> Any:
|
||||
ttl: Optional[int] = None, **kwargs: object) -> Any:
|
||||
"""
|
||||
设置默认值,类似 dict.setdefault()(异步)
|
||||
"""
|
||||
@@ -433,19 +470,26 @@ class AsyncCacheBackend(CacheBackend):
|
||||
pass
|
||||
|
||||
|
||||
class _MemoryTLRUCache(MemoryTLRUCache):
|
||||
class _MemoryTLRUCache(MemoryTLRUCache[str, Any]):
|
||||
"""
|
||||
支持为每个 key 设置独立 TTL 的内存缓存
|
||||
支持为每个 key 设置独立 TTL 的内存缓存。
|
||||
|
||||
:ivar __ttl: 未指定单键存活时间时采用的秒数
|
||||
:ivar __setting_ttls: 写入期间临时保存各键的存活秒数,完成写入后移除
|
||||
"""
|
||||
|
||||
def __init__(self, maxsize: int, ttl: int):
|
||||
"""使用默认 TTL 初始化支持单键过期时间的内存缓存。"""
|
||||
"""
|
||||
使用默认 TTL 初始化支持单键过期时间的内存缓存。
|
||||
"""
|
||||
self.__ttl = ttl
|
||||
self.__setting_ttls: Dict[str, int] = {}
|
||||
super().__init__(maxsize=maxsize, ttu=self._get_expiration)
|
||||
|
||||
def _get_expiration(self, key: str, _value: Any, now: float) -> float:
|
||||
"""计算指定键的绝对过期时间。"""
|
||||
"""
|
||||
计算指定键的绝对过期时间。
|
||||
"""
|
||||
return now + self.__setting_ttls.get(key, self.__ttl)
|
||||
|
||||
@property
|
||||
@@ -474,13 +518,19 @@ class _MemoryTLRUCache(MemoryTLRUCache):
|
||||
|
||||
class MemoryBackend(AtomicCacheBackend):
|
||||
"""
|
||||
基于 `cachetools.TLRUCache` 实现的缓存后端
|
||||
基于 `cachetools.TLRUCache` 实现的缓存后端。
|
||||
|
||||
:cvar _lock: 保护所有区域缓存写入及快照读取的进程锁
|
||||
:cvar _region_caches: 按区域保存共享的有界内存缓存
|
||||
:ivar cache_type: 当前实例创建区域时采用的淘汰策略
|
||||
:ivar maxsize: 新建区域时的默认容量
|
||||
:ivar ttl: 未显式指定单键存活时间时使用的秒数
|
||||
"""
|
||||
|
||||
# 类变量 _region_caches 的互斥锁
|
||||
_lock = threading.Lock()
|
||||
# 存储各个 region 的缓存实例,region -> TLRUCache/LRUCache
|
||||
_region_caches: Dict[str, Union[_MemoryTLRUCache, MemoryLRUCache]] = {}
|
||||
_region_caches: Dict[str, Union[_MemoryTLRUCache, MemoryLRUCache[str, Any]]] = {}
|
||||
|
||||
def __init__(self, cache_type: Literal['ttl', 'lru'] = 'ttl',
|
||||
maxsize: Optional[int] = None, ttl: Optional[int] = None):
|
||||
@@ -495,7 +545,7 @@ class MemoryBackend(AtomicCacheBackend):
|
||||
self.maxsize = maxsize or DEFAULT_CACHE_SIZE
|
||||
self.ttl = DEFAULT_CACHE_TTL if ttl is None else ttl
|
||||
|
||||
def __get_region_cache(self, region: str) -> Optional[Union[_MemoryTLRUCache, MemoryLRUCache]]:
|
||||
def __get_region_cache(self, region: Optional[str]) -> Optional[Union[_MemoryTLRUCache, MemoryLRUCache[str, Any]]]:
|
||||
"""
|
||||
获取指定区域的缓存实例,如果不存在则返回 None
|
||||
"""
|
||||
@@ -503,7 +553,7 @@ class MemoryBackend(AtomicCacheBackend):
|
||||
return self._region_caches.get(region)
|
||||
|
||||
def set(self, key: str, value: Any, ttl: Optional[int] = None,
|
||||
region: Optional[str] = DEFAULT_CACHE_REGION, **kwargs) -> None:
|
||||
region: Optional[str] = DEFAULT_CACHE_REGION, **kwargs: object) -> None:
|
||||
"""
|
||||
设置缓存值支持每个 key 独立配置 TTL
|
||||
|
||||
@@ -513,7 +563,8 @@ class MemoryBackend(AtomicCacheBackend):
|
||||
:param region: 缓存的区
|
||||
"""
|
||||
ttl = self.ttl if ttl is None else ttl
|
||||
maxsize = kwargs.get("maxsize") or self.maxsize
|
||||
# maxsize 按公开写入契约传入整数容量,保留原有真值回退。
|
||||
maxsize = cast(Optional[int], kwargs.get("maxsize")) or self.maxsize
|
||||
region = self.get_region(region)
|
||||
# 设置缓存值
|
||||
with self._lock:
|
||||
@@ -541,7 +592,9 @@ class MemoryBackend(AtomicCacheBackend):
|
||||
region: Optional[str] = DEFAULT_CACHE_REGION,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""严格写入内存缓存。"""
|
||||
"""
|
||||
严格写入内存缓存。
|
||||
"""
|
||||
self.set(key=key, value=value, ttl=ttl, region=region, **kwargs)
|
||||
|
||||
def consume(
|
||||
@@ -549,7 +602,9 @@ class MemoryBackend(AtomicCacheBackend):
|
||||
key: str,
|
||||
region: Optional[str] = DEFAULT_CACHE_REGION,
|
||||
) -> Any:
|
||||
"""在区域缓存锁内原子领取一个值。"""
|
||||
"""
|
||||
在区域缓存锁内原子领取一个值。
|
||||
"""
|
||||
with self._lock:
|
||||
region_cache = self.__get_region_cache(region or DEFAULT_CACHE_REGION)
|
||||
if region_cache is None:
|
||||
@@ -587,7 +642,7 @@ class MemoryBackend(AtomicCacheBackend):
|
||||
with self._lock:
|
||||
return region_cache.get(key)
|
||||
|
||||
def delete(self, key: str, region: Optional[str] = DEFAULT_CACHE_REGION):
|
||||
def delete(self, key: str, region: Optional[str] = DEFAULT_CACHE_REGION) -> None:
|
||||
"""
|
||||
删除缓存
|
||||
|
||||
@@ -647,7 +702,9 @@ class MemoryBackend(AtomicCacheBackend):
|
||||
|
||||
class AsyncMemoryBackend(AsyncCacheBackend):
|
||||
"""
|
||||
基于 `cachetools.TTLCache` 实现的异步缓存后端
|
||||
基于 `cachetools.TTLCache` 实现的异步缓存后端。
|
||||
|
||||
:ivar _backend: 与同步调用共享区域、锁及存储的内存后端
|
||||
"""
|
||||
|
||||
def __init__(self, cache_type: Literal['ttl', 'lru'] = 'ttl',
|
||||
@@ -662,7 +719,7 @@ class AsyncMemoryBackend(AsyncCacheBackend):
|
||||
self._backend = MemoryBackend(cache_type=cache_type, maxsize=maxsize, ttl=ttl)
|
||||
|
||||
async def set(self, key: str, value: Any, ttl: Optional[int] = None,
|
||||
region: Optional[str] = DEFAULT_CACHE_REGION, **kwargs) -> None:
|
||||
region: Optional[str] = DEFAULT_CACHE_REGION, **kwargs: object) -> None:
|
||||
"""
|
||||
设置缓存值支持每个 key 独立配置 TTL
|
||||
|
||||
@@ -693,7 +750,7 @@ class AsyncMemoryBackend(AsyncCacheBackend):
|
||||
"""
|
||||
return self._backend.get(key=key, region=region)
|
||||
|
||||
async def delete(self, key: str, region: Optional[str] = DEFAULT_CACHE_REGION):
|
||||
async def delete(self, key: str, region: Optional[str] = DEFAULT_CACHE_REGION) -> None:
|
||||
"""
|
||||
删除缓存
|
||||
|
||||
@@ -728,11 +785,11 @@ class AsyncMemoryBackend(AsyncCacheBackend):
|
||||
|
||||
|
||||
@contextmanager
|
||||
def fresh(fresh: bool = True):
|
||||
def fresh(fresh: bool = True) -> Generator[None, None, None]:
|
||||
"""
|
||||
是否获取新数据(不使用缓存的值)
|
||||
|
||||
Usage:
|
||||
使用示例:
|
||||
with fresh():
|
||||
result = some_cached_function()
|
||||
"""
|
||||
@@ -744,11 +801,11 @@ def fresh(fresh: bool = True):
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def async_fresh(fresh: bool = True):
|
||||
async def async_fresh(fresh: bool = True) -> AsyncGenerator[None, None]:
|
||||
"""
|
||||
是否获取新数据(不使用缓存的值)
|
||||
|
||||
Usage:
|
||||
使用示例:
|
||||
async with async_fresh():
|
||||
result = await some_async_cached_function()
|
||||
"""
|
||||
@@ -901,6 +958,9 @@ def cached(region: Optional[str] = None, maxsize: Optional[int] = 1024, ttl: Opt
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
"""
|
||||
按函数签名创建保留同步或异步调用方式的缓存包装。
|
||||
"""
|
||||
# 函数签名在装饰后不会变化,预计算可避免每次缓存访问都重复反射。
|
||||
signature = inspect.signature(func)
|
||||
parameter_names = list(signature.parameters.keys())
|
||||
@@ -919,7 +979,6 @@ def cached(region: Optional[str] = None, maxsize: Optional[int] = 1024, ttl: Opt
|
||||
"""
|
||||
if skip_none and value is None:
|
||||
return False
|
||||
# if skip_empty and value in [None, [], {}, "", set()]:
|
||||
if skip_empty and not value:
|
||||
return False
|
||||
if skip_if and skip_if(value):
|
||||
@@ -956,7 +1015,7 @@ def cached(region: Optional[str] = None, maxsize: Optional[int] = 1024, ttl: Opt
|
||||
"""
|
||||
# 如果 skip_none 为 False,且 value 为 None,需要判断缓存实际是否存在
|
||||
if not skip_none and _cached_value is None:
|
||||
if not cache_backend.exists(key=_cache_key, region=_cache_region):
|
||||
if not sync_cache_backend.exists(key=_cache_key, region=_cache_region):
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -971,7 +1030,7 @@ def cached(region: Optional[str] = None, maxsize: Optional[int] = 1024, ttl: Opt
|
||||
"""
|
||||
# 如果 skip_none 为 False,且 value 为 None,需要判断缓存实际是否存在
|
||||
if not skip_none and _cached_value is None:
|
||||
if not await cache_backend.exists(key=_cache_key, region=_cache_region):
|
||||
if not await async_cache_backend.exists(key=_cache_key, region=_cache_region):
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -986,7 +1045,7 @@ def cached(region: Optional[str] = None, maxsize: Optional[int] = 1024, ttl: Opt
|
||||
else func.__name__
|
||||
)
|
||||
|
||||
def __get_cache_key(args, kwargs) -> str:
|
||||
def __get_cache_key(args: tuple[object, ...], kwargs: dict[str, object]) -> str:
|
||||
"""
|
||||
根据函数和参数生成缓存键
|
||||
|
||||
@@ -1021,7 +1080,7 @@ def cached(region: Optional[str] = None, maxsize: Optional[int] = 1024, ttl: Opt
|
||||
|
||||
if is_async:
|
||||
# 异步函数使用异步缓存后端
|
||||
cache_backend = AsyncCache(
|
||||
async_cache_backend = AsyncCache(
|
||||
cache_type="ttl" if ttl is not None or ttl_provider is not None else "lru",
|
||||
maxsize=maxsize,
|
||||
ttl=ttl if ttl is not None else 1,
|
||||
@@ -1031,12 +1090,15 @@ def cached(region: Optional[str] = None, maxsize: Optional[int] = 1024, ttl: Opt
|
||||
|
||||
@wraps(func)
|
||||
async def async_wrapper(*args, **kwargs):
|
||||
"""
|
||||
先读取共享缓存,未命中时等待原函数并按策略写入。
|
||||
"""
|
||||
# 获取缓存键
|
||||
cache_key = __get_cache_key(args, kwargs)
|
||||
|
||||
if not is_fresh():
|
||||
# 尝试获取缓存
|
||||
cached_value = await cache_backend.get(cache_key, region=cache_region)
|
||||
cached_value = await async_cache_backend.get(cache_key, region=cache_region)
|
||||
if should_cache(cached_value) and await async_is_valid_cache_value(cache_key, cached_value,
|
||||
cache_region):
|
||||
return cached_value
|
||||
@@ -1046,41 +1108,41 @@ def cached(region: Optional[str] = None, maxsize: Optional[int] = 1024, ttl: Opt
|
||||
if not should_cache(result):
|
||||
return result
|
||||
# 设置缓存(如果有传入的 maxsize 和 ttl,则覆盖默认值;空结果使用独立的短 TTL)
|
||||
await cache_backend.set(cache_key, result, ttl=get_cache_ttl(result), maxsize=maxsize,
|
||||
await async_cache_backend.set(cache_key, result, ttl=get_cache_ttl(result), maxsize=maxsize,
|
||||
region=cache_region)
|
||||
return result
|
||||
|
||||
async def cache_clear():
|
||||
async def async_cache_clear() -> None:
|
||||
"""
|
||||
清理缓存区
|
||||
"""
|
||||
await cache_backend.clear(region=cache_region)
|
||||
await async_cache_backend.clear(region=cache_region)
|
||||
|
||||
async def cache_exists(*args, **kwargs) -> bool:
|
||||
async def async_cache_exists(*args: object, **kwargs: object) -> bool:
|
||||
"""
|
||||
判断当前参数对应的有效缓存是否存在。
|
||||
"""
|
||||
cache_key = __get_cache_key(args, kwargs)
|
||||
cached_value = await cache_backend.get(cache_key, region=cache_region)
|
||||
cached_value = await async_cache_backend.get(cache_key, region=cache_region)
|
||||
return should_cache(cached_value) and await async_is_valid_cache_value(
|
||||
cache_key, cached_value, cache_region
|
||||
)
|
||||
|
||||
async def cache_delete(*args, **kwargs) -> None:
|
||||
async def async_cache_delete(*args: object, **kwargs: object) -> None:
|
||||
"""
|
||||
删除当前参数对应的缓存。
|
||||
"""
|
||||
cache_key = __get_cache_key(args, kwargs)
|
||||
await cache_backend.delete(cache_key, region=cache_region)
|
||||
await async_cache_backend.delete(cache_key, region=cache_region)
|
||||
|
||||
async_wrapper.cache_region = cache_region
|
||||
async_wrapper.cache_clear = cache_clear
|
||||
async_wrapper.cache_exists = cache_exists
|
||||
async_wrapper.cache_delete = cache_delete
|
||||
async_wrapper.cache_clear = async_cache_clear
|
||||
async_wrapper.cache_exists = async_cache_exists
|
||||
async_wrapper.cache_delete = async_cache_delete
|
||||
return async_wrapper
|
||||
else:
|
||||
# 同步函数使用同步缓存后端
|
||||
cache_backend = Cache(
|
||||
sync_cache_backend = Cache(
|
||||
cache_type="ttl" if ttl is not None or ttl_provider is not None else "lru",
|
||||
maxsize=maxsize,
|
||||
ttl=ttl if ttl is not None else 1,
|
||||
@@ -1090,12 +1152,15 @@ def cached(region: Optional[str] = None, maxsize: Optional[int] = 1024, ttl: Opt
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
"""
|
||||
先读取共享缓存,未命中时执行原函数并按策略写入。
|
||||
"""
|
||||
# 获取缓存键
|
||||
cache_key = __get_cache_key(args, kwargs)
|
||||
|
||||
if not is_fresh():
|
||||
# 尝试获取缓存
|
||||
cached_value = cache_backend.get(cache_key, region=cache_region)
|
||||
cached_value = sync_cache_backend.get(cache_key, region=cache_region)
|
||||
if should_cache(cached_value) and is_valid_cache_value(cache_key, cached_value, cache_region):
|
||||
return cached_value
|
||||
# 执行函数并缓存结果
|
||||
@@ -1104,31 +1169,31 @@ def cached(region: Optional[str] = None, maxsize: Optional[int] = 1024, ttl: Opt
|
||||
if not should_cache(result):
|
||||
return result
|
||||
# 设置缓存(如果有传入的 maxsize 和 ttl,则覆盖默认值;空结果使用独立的短 TTL)
|
||||
cache_backend.set(cache_key, result, ttl=get_cache_ttl(result), maxsize=maxsize, region=cache_region)
|
||||
sync_cache_backend.set(cache_key, result, ttl=get_cache_ttl(result), maxsize=maxsize, region=cache_region)
|
||||
return result
|
||||
|
||||
def cache_clear():
|
||||
def cache_clear() -> None:
|
||||
"""
|
||||
清理缓存区
|
||||
"""
|
||||
cache_backend.clear(region=cache_region)
|
||||
sync_cache_backend.clear(region=cache_region)
|
||||
|
||||
def cache_exists(*args, **kwargs) -> bool:
|
||||
def cache_exists(*args: object, **kwargs: object) -> bool:
|
||||
"""
|
||||
判断当前参数对应的有效缓存是否存在。
|
||||
"""
|
||||
cache_key = __get_cache_key(args, kwargs)
|
||||
cached_value = cache_backend.get(cache_key, region=cache_region)
|
||||
cached_value = sync_cache_backend.get(cache_key, region=cache_region)
|
||||
return should_cache(cached_value) and is_valid_cache_value(
|
||||
cache_key, cached_value, cache_region
|
||||
)
|
||||
|
||||
def cache_delete(*args, **kwargs) -> None:
|
||||
def cache_delete(*args: object, **kwargs: object) -> None:
|
||||
"""
|
||||
删除当前参数对应的缓存。
|
||||
"""
|
||||
cache_key = __get_cache_key(args, kwargs)
|
||||
cache_backend.delete(cache_key, region=cache_region)
|
||||
sync_cache_backend.delete(cache_key, region=cache_region)
|
||||
|
||||
wrapper.cache_region = cache_region
|
||||
wrapper.cache_clear = cache_clear
|
||||
@@ -1141,10 +1206,13 @@ def cached(region: Optional[str] = None, maxsize: Optional[int] = 1024, ttl: Opt
|
||||
|
||||
class CacheProxy:
|
||||
"""
|
||||
缓存代理类,将缓存后端的方法直接代理到实例上
|
||||
缓存代理类,将缓存后端的方法直接代理到实例上。
|
||||
|
||||
:ivar _cache_backend: 实际执行读写与原子领取操作的同步后端
|
||||
:ivar _region: 未传入区域选项时绑定的区域
|
||||
"""
|
||||
|
||||
def __init__(self, cache_backend: AtomicCacheBackend, region: str):
|
||||
def __init__(self, cache_backend: AtomicCacheBackend, region: Optional[str]):
|
||||
"""
|
||||
初始化缓存代理
|
||||
|
||||
@@ -1154,7 +1222,7 @@ class CacheProxy:
|
||||
self._cache_backend = cache_backend
|
||||
self._region = region
|
||||
|
||||
def __getitem__(self, key):
|
||||
def __getitem__(self, key: str) -> Any:
|
||||
"""
|
||||
获取缓存项
|
||||
"""
|
||||
@@ -1163,14 +1231,13 @@ class CacheProxy:
|
||||
raise KeyError(key)
|
||||
return value
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
def __setitem__(self, key: str, value: Any) -> None:
|
||||
"""
|
||||
设置缓存项
|
||||
"""
|
||||
kwargs = {'region': self._region}
|
||||
self._cache_backend.set(key, value, **kwargs)
|
||||
self._cache_backend.set(key, value, region=self._region)
|
||||
|
||||
def __delitem__(self, key):
|
||||
def __delitem__(self, key: str) -> None:
|
||||
"""
|
||||
删除缓存项
|
||||
"""
|
||||
@@ -1178,20 +1245,20 @@ class CacheProxy:
|
||||
raise KeyError(key)
|
||||
self._cache_backend.delete(key, region=self._region)
|
||||
|
||||
def __contains__(self, key):
|
||||
def __contains__(self, key: str) -> bool:
|
||||
"""
|
||||
检查键是否存在
|
||||
"""
|
||||
return self._cache_backend.exists(key, region=self._region)
|
||||
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> Generator[str, None, None]:
|
||||
"""
|
||||
返回缓存的迭代器
|
||||
"""
|
||||
for key, _ in self._cache_backend.items(region=self._region):
|
||||
yield key
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
"""
|
||||
返回缓存项的数量
|
||||
"""
|
||||
@@ -1203,94 +1270,98 @@ class CacheProxy:
|
||||
"""
|
||||
return self._cache_backend.is_redis()
|
||||
|
||||
def get(self, key: str, **kwargs) -> Any:
|
||||
def get(self, key: str, **kwargs: Unpack[_RegionOptions]) -> Any:
|
||||
"""
|
||||
获取缓存值
|
||||
"""
|
||||
kwargs.setdefault('region', self._region)
|
||||
return self._cache_backend.get(key, **kwargs)
|
||||
|
||||
def set(self, key: str, value: Any, **kwargs) -> None:
|
||||
def set(self, key: str, value: Any, **kwargs: Unpack[_WriteOptions]) -> None:
|
||||
"""
|
||||
设置缓存值
|
||||
"""
|
||||
kwargs.setdefault('region', self._region)
|
||||
self._cache_backend.set(key, value, **kwargs)
|
||||
|
||||
def store(self, key: str, value: Any, **kwargs: Any) -> None:
|
||||
"""严格写入缓存,后端故障向调用方传播。"""
|
||||
def store(self, key: str, value: Any, **kwargs: Unpack[_WriteOptions]) -> None:
|
||||
"""
|
||||
严格写入缓存,后端故障向调用方传播。
|
||||
"""
|
||||
kwargs.setdefault('region', self._region)
|
||||
self._cache_backend.store(key, value, **kwargs)
|
||||
|
||||
def consume(self, key: str, **kwargs: Any) -> Any:
|
||||
"""原子领取并删除缓存值。"""
|
||||
def consume(self, key: str, **kwargs: Unpack[_RegionOptions]) -> Any:
|
||||
"""
|
||||
原子领取并删除缓存值。
|
||||
"""
|
||||
kwargs.setdefault('region', self._region)
|
||||
return self._cache_backend.consume(key, **kwargs)
|
||||
|
||||
def delete(self, key: str, **kwargs) -> None:
|
||||
def delete(self, key: str, **kwargs: Unpack[_RegionOptions]) -> None:
|
||||
"""
|
||||
删除缓存值
|
||||
"""
|
||||
kwargs.setdefault('region', self._region)
|
||||
self._cache_backend.delete(key, **kwargs)
|
||||
|
||||
def exists(self, key: str, **kwargs) -> bool:
|
||||
def exists(self, key: str, **kwargs: Unpack[_RegionOptions]) -> bool:
|
||||
"""
|
||||
检查缓存键是否存在
|
||||
"""
|
||||
kwargs.setdefault('region', self._region)
|
||||
return self._cache_backend.exists(key, **kwargs)
|
||||
|
||||
def clear(self, **kwargs) -> None:
|
||||
def clear(self, **kwargs: Unpack[_RegionOptions]) -> None:
|
||||
"""
|
||||
清除缓存
|
||||
"""
|
||||
kwargs.setdefault('region', self._region)
|
||||
self._cache_backend.clear(**kwargs)
|
||||
|
||||
def items(self, **kwargs):
|
||||
def items(self, **kwargs: Unpack[_RegionOptions]) -> Generator[Tuple[str, Any], None, None]:
|
||||
"""
|
||||
获取所有缓存项
|
||||
"""
|
||||
kwargs.setdefault('region', self._region)
|
||||
return self._cache_backend.items(**kwargs)
|
||||
|
||||
def keys(self, **kwargs):
|
||||
def keys(self, **kwargs: Unpack[_RegionOptions]) -> Generator[str, None, None]:
|
||||
"""
|
||||
获取所有缓存键
|
||||
"""
|
||||
kwargs.setdefault('region', self._region)
|
||||
return self._cache_backend.keys(**kwargs)
|
||||
|
||||
def values(self, **kwargs):
|
||||
def values(self, **kwargs: Unpack[_RegionOptions]) -> Generator[Any, None, None]:
|
||||
"""
|
||||
获取所有缓存值
|
||||
"""
|
||||
kwargs.setdefault('region', self._region)
|
||||
return self._cache_backend.values(**kwargs)
|
||||
|
||||
def update(self, other: Dict[str, Any], **kwargs) -> None:
|
||||
def update(self, other: Dict[str, Any], **kwargs: Unpack[_WriteOptions]) -> None:
|
||||
"""
|
||||
更新缓存
|
||||
"""
|
||||
kwargs.setdefault('region', self._region)
|
||||
self._cache_backend.update(other, **kwargs)
|
||||
|
||||
def pop(self, key: str, default: Any = None, **kwargs) -> Any:
|
||||
def pop(self, key: str, default: Any = None, **kwargs: Unpack[_RegionOptions]) -> Any:
|
||||
"""
|
||||
弹出缓存项
|
||||
"""
|
||||
kwargs.setdefault('region', self._region)
|
||||
return self._cache_backend.pop(key, default, **kwargs)
|
||||
|
||||
def popitem(self, **kwargs) -> Tuple[str, Any]:
|
||||
def popitem(self, **kwargs: Unpack[_RegionOptions]) -> Tuple[str, Any]:
|
||||
"""
|
||||
弹出最后一个缓存项
|
||||
"""
|
||||
kwargs.setdefault('region', self._region)
|
||||
return self._cache_backend.popitem(**kwargs)
|
||||
|
||||
def setdefault(self, key: str, default: Any = None, **kwargs) -> Any:
|
||||
def setdefault(self, key: str, default: Any = None, **kwargs: Unpack[_WriteOptions]) -> Any:
|
||||
"""
|
||||
设置默认值
|
||||
"""
|
||||
@@ -1309,9 +1380,12 @@ class AsyncCacheProxy:
|
||||
异步缓存代理类,将异步缓存后端的方法直接代理到实例上
|
||||
|
||||
与同步 CacheProxy 的唯一差异是方法均为 async,默认绑定构造时指定的 region。
|
||||
|
||||
:ivar _cache_backend: 实际执行异步读写的缓存后端
|
||||
:ivar _region: 未传入区域选项时绑定的区域
|
||||
"""
|
||||
|
||||
def __init__(self, cache_backend: AsyncCacheBackend, region: str):
|
||||
def __init__(self, cache_backend: AsyncCacheBackend, region: Optional[str]):
|
||||
"""
|
||||
初始化异步缓存代理
|
||||
|
||||
@@ -1327,42 +1401,42 @@ class AsyncCacheProxy:
|
||||
"""
|
||||
return self._cache_backend.is_redis()
|
||||
|
||||
async def get(self, key: str, **kwargs) -> Any:
|
||||
async def get(self, key: str, **kwargs: Unpack[_RegionOptions]) -> Any:
|
||||
"""
|
||||
获取缓存值
|
||||
"""
|
||||
kwargs.setdefault('region', self._region)
|
||||
return await self._cache_backend.get(key, **kwargs)
|
||||
|
||||
async def set(self, key: str, value: Any, **kwargs) -> None:
|
||||
async def set(self, key: str, value: Any, **kwargs: Unpack[_WriteOptions]) -> None:
|
||||
"""
|
||||
设置缓存值
|
||||
"""
|
||||
kwargs.setdefault('region', self._region)
|
||||
await self._cache_backend.set(key, value, **kwargs)
|
||||
|
||||
async def delete(self, key: str, **kwargs) -> None:
|
||||
async def delete(self, key: str, **kwargs: Unpack[_RegionOptions]) -> None:
|
||||
"""
|
||||
删除缓存值
|
||||
"""
|
||||
kwargs.setdefault('region', self._region)
|
||||
await self._cache_backend.delete(key, **kwargs)
|
||||
|
||||
async def exists(self, key: str, **kwargs) -> bool:
|
||||
async def exists(self, key: str, **kwargs: Unpack[_RegionOptions]) -> bool:
|
||||
"""
|
||||
检查缓存键是否存在
|
||||
"""
|
||||
kwargs.setdefault('region', self._region)
|
||||
return await self._cache_backend.exists(key, **kwargs)
|
||||
|
||||
async def clear(self, **kwargs) -> None:
|
||||
async def clear(self, **kwargs: Unpack[_RegionOptions]) -> None:
|
||||
"""
|
||||
清除缓存
|
||||
"""
|
||||
kwargs.setdefault('region', self._region)
|
||||
await self._cache_backend.clear(**kwargs)
|
||||
|
||||
async def items(self, **kwargs):
|
||||
async def items(self, **kwargs: Unpack[_RegionOptions]) -> AsyncGenerator[Tuple[str, Any], None]:
|
||||
"""
|
||||
获取所有缓存项
|
||||
"""
|
||||
@@ -1370,7 +1444,7 @@ class AsyncCacheProxy:
|
||||
async for item in self._cache_backend.items(**kwargs):
|
||||
yield item
|
||||
|
||||
async def keys(self, **kwargs):
|
||||
async def keys(self, **kwargs: Unpack[_RegionOptions]) -> AsyncGenerator[str, None]:
|
||||
"""
|
||||
获取所有缓存键
|
||||
"""
|
||||
@@ -1378,7 +1452,7 @@ class AsyncCacheProxy:
|
||||
async for key in self._cache_backend.keys(**kwargs):
|
||||
yield key
|
||||
|
||||
async def values(self, **kwargs):
|
||||
async def values(self, **kwargs: Unpack[_RegionOptions]) -> AsyncGenerator[Any, None]:
|
||||
"""
|
||||
获取所有缓存值
|
||||
"""
|
||||
@@ -1386,28 +1460,28 @@ class AsyncCacheProxy:
|
||||
async for value in self._cache_backend.values(**kwargs):
|
||||
yield value
|
||||
|
||||
async def update(self, other: Dict[str, Any], **kwargs) -> None:
|
||||
async def update(self, other: Dict[str, Any], **kwargs: Unpack[_WriteOptions]) -> None:
|
||||
"""
|
||||
更新缓存
|
||||
"""
|
||||
kwargs.setdefault('region', self._region)
|
||||
await self._cache_backend.update(other, **kwargs)
|
||||
|
||||
async def pop(self, key: str, default: Any = None, **kwargs) -> Any:
|
||||
async def pop(self, key: str, default: Any = None, **kwargs: Unpack[_RegionOptions]) -> Any:
|
||||
"""
|
||||
弹出缓存项
|
||||
"""
|
||||
kwargs.setdefault('region', self._region)
|
||||
return await self._cache_backend.pop(key, default, **kwargs)
|
||||
|
||||
async def popitem(self, **kwargs) -> Tuple[str, Any]:
|
||||
async def popitem(self, **kwargs: Unpack[_RegionOptions]) -> Tuple[str, Any]:
|
||||
"""
|
||||
弹出最后一个缓存项
|
||||
"""
|
||||
kwargs.setdefault('region', self._region)
|
||||
return await self._cache_backend.popitem(**kwargs)
|
||||
|
||||
async def setdefault(self, key: str, default: Any = None, **kwargs) -> Any:
|
||||
async def setdefault(self, key: str, default: Any = None, **kwargs: Unpack[_WriteOptions]) -> Any:
|
||||
"""
|
||||
设置默认值
|
||||
"""
|
||||
|
||||
104
tests/test_runtime_cache_types.py
Normal file
104
tests/test_runtime_cache_types.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
缓存类型收敛期间的区域转发、异步元数据与上下文恢复契约。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from app.runtime.cache import (
|
||||
AsyncCacheProxy,
|
||||
AsyncMemoryBackend,
|
||||
CacheProxy,
|
||||
MemoryBackend,
|
||||
async_fresh,
|
||||
cached,
|
||||
fresh,
|
||||
is_fresh,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def isolated_regions(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""
|
||||
每个用例使用独立区域表,结束后恢复进程原有缓存。
|
||||
"""
|
||||
monkeypatch.setattr(MemoryBackend, "_region_caches", {})
|
||||
|
||||
|
||||
def test_proxy_region_override_and_zero_ttl() -> None:
|
||||
"""
|
||||
显式区域覆盖与零 TTL 保持原有转发语义,映射操作仍使用绑定区域。
|
||||
"""
|
||||
backend = MemoryBackend(ttl=60)
|
||||
proxy = CacheProxy(backend, None)
|
||||
proxy["bound"] = b"binary"
|
||||
proxy.set("other", 2, region="alternate", maxsize=2, ttl=60)
|
||||
assert list(proxy.items()) == [("bound", b"binary")]
|
||||
assert list(proxy.keys(region="alternate")) == ["other"]
|
||||
assert list(proxy.values(region="alternate")) == [2]
|
||||
proxy.set("other", 3, region="alternate", ttl=0)
|
||||
assert not proxy.exists("other", region="alternate")
|
||||
assert proxy.pop("bound") == b"binary"
|
||||
assert len(proxy) == 0
|
||||
|
||||
|
||||
def test_async_proxy_and_decorator_management() -> None:
|
||||
"""
|
||||
异步代理迭代与缓存管理回调仍可等待,删除操作只影响指定参数键。
|
||||
"""
|
||||
calls = []
|
||||
|
||||
@cached(region="typed-wrapper", local_only=True)
|
||||
async def fetch(key: str) -> str:
|
||||
"""
|
||||
记录未命中缓存时发生的实际调用。
|
||||
"""
|
||||
calls.append(key)
|
||||
return key.upper()
|
||||
|
||||
async def exercise() -> None:
|
||||
"""
|
||||
在单一事件循环中验证代理和装饰器的异步契约。
|
||||
"""
|
||||
proxy = AsyncCacheProxy(AsyncMemoryBackend(), None)
|
||||
await proxy.set("item", b"data", region="alternate", maxsize=2)
|
||||
assert [item async for item in proxy.items(region="alternate")] == [("item", b"data")]
|
||||
assert [key async for key in proxy.keys(region="alternate")] == ["item"]
|
||||
assert [value async for value in proxy.values(region="alternate")] == [b"data"]
|
||||
assert await fetch("a") == "A"
|
||||
assert await fetch("a") == "A"
|
||||
assert calls == ["a"]
|
||||
assert await fetch.cache_exists("a")
|
||||
await fetch.cache_delete("a")
|
||||
assert not await fetch.cache_exists("a")
|
||||
await fetch("b")
|
||||
await fetch.cache_clear()
|
||||
assert not await fetch.cache_exists("b")
|
||||
|
||||
asyncio.run(exercise())
|
||||
|
||||
|
||||
def test_fresh_context_restores_after_exception() -> None:
|
||||
"""
|
||||
同步和异步嵌套刷新上下文在异常退出后都恢复外层状态。
|
||||
"""
|
||||
original = is_fresh()
|
||||
with pytest.raises(ValueError), fresh():
|
||||
with fresh(False):
|
||||
assert is_fresh()
|
||||
raise ValueError("测试退出")
|
||||
assert is_fresh() is original
|
||||
|
||||
async def exercise() -> None:
|
||||
"""
|
||||
异步上下文在异常和嵌套退出时恢复令牌。
|
||||
"""
|
||||
with pytest.raises(ValueError):
|
||||
async with async_fresh():
|
||||
async with async_fresh(False):
|
||||
assert is_fresh()
|
||||
raise ValueError("测试退出")
|
||||
assert is_fresh() is original
|
||||
|
||||
asyncio.run(exercise())
|
||||
Reference in New Issue
Block a user