mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-07 08:26:53 +08:00
refactor(database): isolate configuration transactions
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import threading
|
||||
import uuid
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -16,15 +17,105 @@ def _unique_key() -> str:
|
||||
|
||||
|
||||
def _fresh_oper() -> SystemConfigOper:
|
||||
"""重置单例并从数据库重新加载配置缓存。"""
|
||||
"""重置单例并显式加载数据库配置快照。"""
|
||||
Singleton._instances.pop((SystemConfigOper, (), frozenset()), None)
|
||||
return SystemConfigOper()
|
||||
oper = SystemConfigOper()
|
||||
oper.load_snapshot()
|
||||
return oper
|
||||
|
||||
|
||||
def test_constructor_does_not_query_database(monkeypatch):
|
||||
"""构造配置对象时不打开数据库会话。"""
|
||||
Singleton._instances.pop((SystemConfigOper, (), frozenset()), None)
|
||||
monkeypatch.setattr(
|
||||
SystemConfig,
|
||||
"list",
|
||||
lambda _db: pytest.fail("构造阶段不应查询数据库"),
|
||||
)
|
||||
|
||||
oper = SystemConfigOper()
|
||||
|
||||
with pytest.raises(RuntimeError, match="快照尚未加载"):
|
||||
oper.get("key")
|
||||
|
||||
|
||||
def test_load_snapshot_publishes_complete_dictionary(monkeypatch):
|
||||
"""重新加载期间读取方只会看到完整旧快照或完整新快照。"""
|
||||
Singleton._instances.pop((SystemConfigOper, (), frozenset()), None)
|
||||
oper = SystemConfigOper()
|
||||
values = [SimpleNamespace(key="key", value="old")]
|
||||
entered = threading.Event()
|
||||
release = threading.Event()
|
||||
|
||||
monkeypatch.setattr(SystemConfig, "list", lambda _db: values)
|
||||
oper.load_snapshot()
|
||||
|
||||
def load_new_snapshot(_db):
|
||||
entered.set()
|
||||
release.wait(1)
|
||||
return [SimpleNamespace(key="key", value="new")]
|
||||
|
||||
monkeypatch.setattr(SystemConfig, "list", load_new_snapshot)
|
||||
thread = threading.Thread(target=oper.load_snapshot)
|
||||
thread.start()
|
||||
assert entered.wait(1)
|
||||
|
||||
assert oper.get("key") == "old"
|
||||
|
||||
release.set()
|
||||
thread.join(1)
|
||||
assert thread.is_alive() is False
|
||||
assert oper.get("key") == "new"
|
||||
|
||||
|
||||
def test_read_does_not_wait_for_slow_write_transaction(monkeypatch):
|
||||
"""数据库写入期间,内存读取仍返回最近一次已提交值。"""
|
||||
oper = _fresh_oper()
|
||||
key = _unique_key()
|
||||
oper.set(key, "old")
|
||||
entered = threading.Event()
|
||||
release = threading.Event()
|
||||
|
||||
def slow_write(_operation):
|
||||
entered.set()
|
||||
release.wait(1)
|
||||
return True
|
||||
|
||||
monkeypatch.setattr(oper, "_execute_sync_write", slow_write)
|
||||
thread = threading.Thread(target=lambda: oper.set(key, "new"))
|
||||
thread.start()
|
||||
assert entered.wait(1)
|
||||
|
||||
assert oper.get(key) == "old"
|
||||
|
||||
release.set()
|
||||
thread.join(1)
|
||||
assert thread.is_alive() is False
|
||||
assert oper.get(key) == "new"
|
||||
|
||||
|
||||
def test_failed_write_keeps_committed_snapshot(monkeypatch):
|
||||
"""事务失败时内存快照保持最近一次已提交值。"""
|
||||
oper = _fresh_oper()
|
||||
key = _unique_key()
|
||||
oper.set(key, "old")
|
||||
|
||||
def fail_write(_operation):
|
||||
raise RuntimeError("write failed")
|
||||
|
||||
monkeypatch.setattr(oper, "_execute_sync_write", fail_write)
|
||||
|
||||
with pytest.raises(RuntimeError, match="write failed"):
|
||||
oper.set(key, "new")
|
||||
assert oper.get(key) == "old"
|
||||
|
||||
|
||||
def test_increment_serializes_concurrent_counter_updates(monkeypatch):
|
||||
"""并发递增系统计数时不应丢失更新。"""
|
||||
oper = object.__new__(SystemConfigOper)
|
||||
oper._rlock = threading.RLock()
|
||||
oper._snapshot_lock = threading.RLock()
|
||||
oper._write_lock = threading.RLock()
|
||||
oper._loaded = True
|
||||
stored_value = {"value": 0}
|
||||
|
||||
monkeypatch.setattr(oper, "get", lambda _key: stored_value["value"])
|
||||
@@ -51,7 +142,9 @@ def test_increment_serializes_concurrent_counter_updates(monkeypatch):
|
||||
def test_increment_supports_custom_step(monkeypatch):
|
||||
"""整数系统计数应支持指定递增步长。"""
|
||||
oper = object.__new__(SystemConfigOper)
|
||||
oper._rlock = threading.RLock()
|
||||
oper._snapshot_lock = threading.RLock()
|
||||
oper._write_lock = threading.RLock()
|
||||
oper._loaded = True
|
||||
stored_value = {"value": 4}
|
||||
|
||||
monkeypatch.setattr(oper, "get", lambda _key: stored_value["value"])
|
||||
@@ -95,7 +188,17 @@ async def test_async_set_persists_falsy_value_on_existing_record():
|
||||
oper = _fresh_oper()
|
||||
|
||||
oper.set(key, True)
|
||||
assert await oper.async_set(key, False) is True
|
||||
from app.application.configuration import SystemConfigService
|
||||
|
||||
class _InlineDatabaseExecutor:
|
||||
async def run(self, operation):
|
||||
return operation()
|
||||
|
||||
service = SystemConfigService(
|
||||
repository=oper,
|
||||
async_executor=_InlineDatabaseExecutor(),
|
||||
)
|
||||
assert await service.async_set(key, False) is True
|
||||
assert oper.get(key) is False
|
||||
assert SystemConfig.get_by_key(oper._db, key).value is False
|
||||
|
||||
@@ -106,7 +209,17 @@ async def test_async_set_creates_record_for_falsy_value():
|
||||
key = _unique_key()
|
||||
oper = _fresh_oper()
|
||||
|
||||
assert await oper.async_set(key, 0) is True
|
||||
from app.application.configuration import SystemConfigService
|
||||
|
||||
class _InlineDatabaseExecutor:
|
||||
async def run(self, operation):
|
||||
return operation()
|
||||
|
||||
service = SystemConfigService(
|
||||
repository=oper,
|
||||
async_executor=_InlineDatabaseExecutor(),
|
||||
)
|
||||
assert await service.async_set(key, 0) is True
|
||||
assert oper.get(key) == 0
|
||||
assert SystemConfig.get_by_key(oper._db, key).value == 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user