提交
This commit is contained in:
94
backend/tests/test_cache_local.py
Normal file
94
backend/tests/test_cache_local.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""cache.py 本地层(不碰 Redis):TTL 过期、容量淘汰、熔断冷却恢复。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
import app.cache as cache_mod
|
||||
from app import cache
|
||||
|
||||
|
||||
class _FakeClock:
|
||||
"""替换 cache 命名空间里的 time(不影响全局 time 模块)。"""
|
||||
|
||||
def __init__(self):
|
||||
self.now = 1000.0
|
||||
|
||||
def monotonic(self) -> float:
|
||||
return self.now
|
||||
|
||||
def time(self) -> float:
|
||||
return self.now
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_state():
|
||||
"""每个用例干净的本地缓存状态。"""
|
||||
cache._local_store.clear()
|
||||
cache._local_bytes = 0
|
||||
cache._disabled_until = 0.0
|
||||
yield
|
||||
cache._local_store.clear()
|
||||
cache._local_bytes = 0
|
||||
cache._disabled_until = 0.0
|
||||
|
||||
|
||||
def test_local_set_get_roundtrip():
|
||||
cache.local_set("k", '{"a":1}', ttl=60)
|
||||
assert cache.local_get("k") == '{"a":1}'
|
||||
|
||||
|
||||
def test_local_get_miss():
|
||||
assert cache.local_get("nope") is None
|
||||
|
||||
|
||||
def test_local_expiry(monkeypatch):
|
||||
clock = _FakeClock()
|
||||
monkeypatch.setattr(cache_mod, "time", clock)
|
||||
cache.local_set("k", "v", ttl=10)
|
||||
clock.now = 1005.0
|
||||
assert cache.local_get("k") == "v"
|
||||
clock.now = 1101.0 # 过期
|
||||
assert cache.local_get("k") is None
|
||||
assert "k" not in cache._local_store # 过期读取顺手清理
|
||||
|
||||
|
||||
def test_local_ttl_capped_at_120s():
|
||||
"""本地层恒 ≤120s(多进程部署时最多比 Redis 多陈旧 120s 的约定)。"""
|
||||
base = cache.time.monotonic()
|
||||
cache.local_set("k", "v", ttl=99999)
|
||||
ent = cache._local_store["k"]
|
||||
assert ent[0] - base <= 120.0 + 5 # 相对当前 monotonic 的上限(留误差余量)
|
||||
|
||||
|
||||
def test_local_entries_cap_evicts_oldest():
|
||||
for i in range(cache._LOCAL_MAX_ENTRIES + 5):
|
||||
cache.local_set(f"k{i}", "v", ttl=60)
|
||||
assert len(cache._local_store) <= cache._LOCAL_MAX_ENTRIES
|
||||
# 先插入的(最旧)被近似 LRU 淘汰
|
||||
assert cache.local_get("k0") is None
|
||||
assert cache.local_get(f"k{cache._LOCAL_MAX_ENTRIES + 4}") == "v"
|
||||
|
||||
|
||||
def test_local_overwrite_releases_bytes():
|
||||
cache.local_set("k", "x" * 1000, ttl=60)
|
||||
before = cache._local_bytes
|
||||
cache.local_set("k", "y", ttl=60)
|
||||
assert cache._local_bytes < before
|
||||
assert cache.local_get("k") == "y"
|
||||
|
||||
|
||||
def test_bail_cooldown_recovers(monkeypatch):
|
||||
"""熔断 60s:期间 _client 为 None,到期自动放行(Redis 未配置时也返回 None,但不熔断)。"""
|
||||
clock = _FakeClock()
|
||||
monkeypatch.setattr(cache_mod, "time", clock)
|
||||
cache._bail()
|
||||
assert cache._disabled_until > clock.now
|
||||
clock.now += 59.0
|
||||
assert clock.monotonic() < cache._disabled_until # 仍在熔断期
|
||||
clock.now += 2.0 # 越过 60s 冷却
|
||||
assert clock.monotonic() >= cache._disabled_until # 恢复探测资格
|
||||
|
||||
|
||||
def test_digest_stable_and_distinct():
|
||||
assert cache.digest("a", 1, None) == cache.digest("a", 1, None)
|
||||
assert cache.digest("a", 1) != cache.digest("a", 2)
|
||||
Reference in New Issue
Block a user