From 4ca3e405079958300ba9ad5d3e0cc3ea7aa49892 Mon Sep 17 00:00:00 2001 From: Ekko Date: Tue, 21 Jul 2026 08:10:20 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=85=B1=E4=BA=AB=E5=86=85?= =?UTF-8?q?=E5=AD=98=E7=BC=93=E5=AD=98=E7=9A=84=E9=80=90=E9=94=AE=20TTL=20?= =?UTF-8?q?(#6161)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MemoryBackend 原先为每个 region 保存一个 cachetools.TTLCache,导致首次写入的 TTL 成为后续所有 key 的统一生命周期。p115strmhelper 等动态 TTL 调用方因此可能把短期有效的 URL 缓存数天。 改用 TLRUCache 为每次写入独立计算过期时间,同时保留同步与异步 backend 共享 region、缓存值原样返回等既有行为。 Co-authored-by: Claude Fable 5 Co-authored-by: jxxghp --- tests/test_cache_system.py | 90 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/tests/test_cache_system.py b/tests/test_cache_system.py index 436c2d58b..fd1f812ca 100644 --- a/tests/test_cache_system.py +++ b/tests/test_cache_system.py @@ -490,6 +490,96 @@ def test_file_cache_uses_default_ttl_when_omitted(monkeypatch): assert AsyncFileCache().ttl == 7 * 24 * 3600 +def test_memory_backend_supports_per_key_ttl(): + """ + 同一 region 的 key 应按各自 TTL 过期,不受首个 key 的 TTL 影响。 + """ + region = "per_key_ttl" + cache = MemoryBackend() + cache.set("short", "short-value", ttl=10, region=region) + cache.set("long", "long-value", ttl=20, region=region) + region_cache = MemoryBackend._region_caches[cache.get_region(region)] + started_at = region_cache.timer() + + region_cache.expire(time=started_at + 11) + + assert cache.get("short", region=region) is None + assert cache.get("long", region=region) == "long-value" + assert list(cache.items(region=region)) == [("long", "long-value")] + + +def test_memory_backend_resets_ttl_when_key_is_rewritten(): + """ + 重写已有 key 时应从重写时刻按新 TTL 重新计算过期时间。 + """ + region = "rewrite_per_key_ttl" + cache = MemoryBackend() + cache.set("key", "old", ttl=10, region=region) + region_cache = MemoryBackend._region_caches[cache.get_region(region)] + started_at = region_cache.timer() + + cache.set("key", "new", ttl=20, region=region) + region_cache.expire(time=started_at + 11) + + assert cache.get("key", region=region) == "new" + + region_cache.expire(time=started_at + 21) + + assert cache.get("key", region=region) is None + + +def test_memory_backend_instances_share_region_with_per_key_ttl(): + """ + 多个 backend 仍共享 region 数据,但每次写入的显式 TTL 应独立生效。 + """ + region = "shared_per_key_ttl" + first = MemoryBackend() + second = MemoryBackend() + first.set("first", "first-value", ttl=10, region=region) + second.set("second", "second-value", ttl=20, region=region) + region_cache = MemoryBackend._region_caches[first.get_region(region)] + started_at = region_cache.timer() + + region_cache.expire(time=started_at + 11) + + assert second.get("first", region=region) is None + assert first.get("second", region=region) == "second-value" + + +def test_async_memory_backend_supports_per_key_ttl(): + """ + 异步代理路径应与同步 backend 共享 region,并保留每个 key 的 TTL。 + """ + async def run_test(): + region = "async_per_key_ttl" + sync_cache = MemoryBackend() + async_cache = AsyncMemoryBackend() + await async_cache.set("short", "short-value", ttl=10, region=region) + sync_cache.set("long", "long-value", ttl=20, region=region) + region_cache = MemoryBackend._region_caches[sync_cache.get_region(region)] + started_at = region_cache.timer() + region_cache.expire(time=started_at + 11) + + assert await async_cache.get("short", region=region) is None + assert await async_cache.get("long", region=region) == "long-value" + + asyncio.run(run_test()) + + +def test_memory_lru_backend_keeps_capacity_eviction_behavior(): + """ + per-key TTL 改造不应影响 LRU region 的容量淘汰行为。 + """ + region = "memory_lru" + cache = MemoryBackend(cache_type="lru", maxsize=2) + cache.set("first", 1, region=region) + cache.set("second", 2, region=region) + cache.set("third", 3, region=region) + + assert cache.get("first", region=region) is None + assert list(cache.items(region=region)) == [("second", 2), ("third", 3)] + + def test_redis_original_key_decodes_quoted_key(): """ Redis items 返回的 key 应还原为原始缓存 key,确保带特殊字符的 key 可继续删除。