mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 23:17:20 +08:00
refactor: close transactional boundary debt batch
This commit is contained in:
+110
-1
@@ -2,6 +2,7 @@ import asyncio
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
@@ -13,15 +14,17 @@ from app.adapters.cache.backends import (
|
||||
FileBackend,
|
||||
RedisBackend,
|
||||
)
|
||||
from app.adapters.cache.redis import AsyncRedisHelper, RedisHelper, serialize
|
||||
from app.runtime.cache import (
|
||||
AsyncFileCache,
|
||||
AsyncMemoryBackend,
|
||||
FileCache,
|
||||
MemoryBackend,
|
||||
TTLCache,
|
||||
cached,
|
||||
)
|
||||
from app.runtime.config import settings
|
||||
from app.adapters.cache.redis import AsyncRedisHelper, RedisHelper, serialize
|
||||
|
||||
|
||||
def test_file_backend_items_keep_relative_keys_and_bytes(tmp_path):
|
||||
"""
|
||||
@@ -720,6 +723,112 @@ def test_redis_helper_pop_uses_atomic_getdel():
|
||||
assert calls == ["region:passkey_challenge:key:token"]
|
||||
|
||||
|
||||
def test_redis_helper_strict_consume_propagates_backend_failure():
|
||||
"""严格领取必须区分 Redis 故障与键不存在,旧 pop 仍保持兼容返回值。"""
|
||||
class FailingClient:
|
||||
"""模拟 GETDEL 连接故障。"""
|
||||
|
||||
def getdel(self, _key):
|
||||
"""报告 Redis 命令失败。"""
|
||||
raise ConnectionError("redis unavailable")
|
||||
|
||||
helper = RedisHelper()
|
||||
helper.client = FailingClient()
|
||||
try:
|
||||
with pytest.raises(ConnectionError, match="redis unavailable"):
|
||||
helper.consume("token", region="passkey_challenge")
|
||||
assert helper.pop("token", region="passkey_challenge") is None
|
||||
finally:
|
||||
helper.client = None
|
||||
|
||||
|
||||
def test_redis_helper_strict_store_requires_backend_acknowledgement():
|
||||
"""严格写入不得把 Redis 未确认写入当成成功签发。"""
|
||||
class RejectingClient:
|
||||
"""模拟 Redis 拒绝确认 SET。"""
|
||||
|
||||
def set(self, *_args, **_kwargs):
|
||||
"""返回未写入状态。"""
|
||||
return False
|
||||
|
||||
helper = RedisHelper()
|
||||
helper.client = RejectingClient()
|
||||
try:
|
||||
with pytest.raises(RuntimeError, match="not acknowledged"):
|
||||
helper.store("token", "challenge", region="passkey_challenge")
|
||||
helper.set("token", "challenge", region="passkey_challenge")
|
||||
finally:
|
||||
helper.client = None
|
||||
|
||||
|
||||
def test_memory_atomic_cache_consume_has_single_winner_and_honors_ttl():
|
||||
"""内存原子缓存与 Redis 一样只允许一次领取并服从 TTL。"""
|
||||
cache = MemoryBackend(ttl=60)
|
||||
region = "atomic_memory_contract"
|
||||
cache.clear(region=region)
|
||||
cache.store("token", "challenge", region=region)
|
||||
|
||||
with ThreadPoolExecutor(max_workers=8) as executor:
|
||||
results = list(executor.map(
|
||||
lambda _: cache.consume("token", region=region),
|
||||
range(8),
|
||||
))
|
||||
|
||||
assert results.count("challenge") == 1
|
||||
assert results.count(None) == 7
|
||||
|
||||
cache.store("expired", "challenge", ttl=0, region=region)
|
||||
assert cache.consume("expired", region=region) is None
|
||||
|
||||
|
||||
def test_ttl_cache_legacy_pop_uses_atomic_consume_contract():
|
||||
"""插件既有 TTLCache.pop 入口保持可用并获得原子领取语义。"""
|
||||
cache = TTLCache(region="legacy_atomic_pop", maxsize=8, ttl=60)
|
||||
cache.clear()
|
||||
cache.set("token", "challenge")
|
||||
|
||||
def pop_token(_index):
|
||||
"""兼容入口不存在时返回空值,便于汇总并发结果。"""
|
||||
try:
|
||||
return cache.pop("token")
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
with ThreadPoolExecutor(max_workers=8) as executor:
|
||||
results = list(executor.map(pop_token, range(8)))
|
||||
|
||||
assert results.count("challenge") == 1
|
||||
assert results.count(None) == 7
|
||||
|
||||
|
||||
def test_redis_backend_strict_store_and_consume_use_atomic_helper():
|
||||
"""Redis 适配器将严格缓存契约完整委托给底层原子实现。"""
|
||||
calls = []
|
||||
|
||||
class FakeHelper:
|
||||
"""记录严格 Redis 缓存调用。"""
|
||||
|
||||
def store(self, key, value, ttl=None, region=None, **kwargs):
|
||||
"""记录严格写入。"""
|
||||
calls.append(("store", key, value, ttl, region, kwargs))
|
||||
|
||||
def consume(self, key, region=None):
|
||||
"""记录原子领取。"""
|
||||
calls.append(("consume", key, region))
|
||||
return "challenge"
|
||||
|
||||
backend = RedisBackend(ttl=60)
|
||||
backend.redis_helper = FakeHelper()
|
||||
|
||||
backend.store("token", "challenge", region="passkey_challenge")
|
||||
assert backend.consume("token", region="passkey_challenge") == "challenge"
|
||||
|
||||
assert calls == [
|
||||
("store", "token", "challenge", 60, "passkey_challenge", {}),
|
||||
("consume", "token", "passkey_challenge"),
|
||||
]
|
||||
|
||||
|
||||
def test_async_redis_helper_uses_blocking_pool_settings(monkeypatch):
|
||||
"""
|
||||
Redis 异步客户端应使用阻塞连接池,避免高并发缓存读取立刻抛出连接耗尽错误。
|
||||
|
||||
Reference in New Issue
Block a user