mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-06 16:07:01 +08:00
refactor: close transactional boundary debt batch
This commit is contained in:
+80
-7
@@ -3,10 +3,10 @@ import inspect
|
||||
import logging
|
||||
import threading
|
||||
from abc import ABC, abstractmethod
|
||||
from contextlib import contextmanager, asynccontextmanager
|
||||
from contextlib import asynccontextmanager, contextmanager
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, Optional, Generator, AsyncGenerator, Tuple, Literal, Union
|
||||
from typing import Any, AsyncGenerator, Callable, Dict, Generator, Literal, Optional, Tuple, Union
|
||||
|
||||
from cachetools import LRUCache as MemoryLRUCache
|
||||
from cachetools import TLRUCache as MemoryTLRUCache
|
||||
@@ -21,7 +21,7 @@ DEFAULT_CACHE_TTL = 365 * 24 * 60 * 60
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_backend_type_provider: Callable[[], str] = lambda: "memory"
|
||||
_redis_factory: Optional[Callable[[Optional[int]], "CacheBackend"]] = None
|
||||
_redis_factory: Optional[Callable[[Optional[int]], "AtomicCacheBackend"]] = None
|
||||
_async_redis_factory: Optional[
|
||||
Callable[[Optional[int]], "AsyncCacheBackend"]
|
||||
] = None
|
||||
@@ -35,7 +35,7 @@ _file_ttl_provider: Callable[[], int] = lambda: DEFAULT_CACHE_TTL
|
||||
def configure_cache_factories(
|
||||
*,
|
||||
backend_type_provider: Callable[[], str],
|
||||
redis_factory: Callable[[Optional[int]], "CacheBackend"],
|
||||
redis_factory: Callable[[Optional[int]], "AtomicCacheBackend"],
|
||||
async_redis_factory: Callable[[Optional[int]], "AsyncCacheBackend"],
|
||||
file_factory: Callable[[Optional[Path]], "CacheBackend"],
|
||||
async_file_factory: Callable[[Optional[Path]], "AsyncCacheBackend"],
|
||||
@@ -245,6 +245,43 @@ class CacheBackend(ABC):
|
||||
return False
|
||||
|
||||
|
||||
class AtomicCacheBackend(CacheBackend):
|
||||
"""支持严格写入和原子领取的一次性缓存后端契约。"""
|
||||
|
||||
@abstractmethod
|
||||
def store(
|
||||
self,
|
||||
key: str,
|
||||
value: Any,
|
||||
ttl: Optional[int] = None,
|
||||
region: Optional[str] = DEFAULT_CACHE_REGION,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""写入缓存;后端故障必须向调用方传播。"""
|
||||
|
||||
@abstractmethod
|
||||
def consume(
|
||||
self,
|
||||
key: str,
|
||||
region: Optional[str] = DEFAULT_CACHE_REGION,
|
||||
) -> Any:
|
||||
"""原子读取并删除缓存值,不存在时返回 None。"""
|
||||
|
||||
def pop(
|
||||
self,
|
||||
key: str,
|
||||
default: Any = None,
|
||||
region: Optional[str] = DEFAULT_CACHE_REGION,
|
||||
) -> Any:
|
||||
"""以原子领取实现兼容的字典 pop 语义。"""
|
||||
value = self.consume(key=key, region=region)
|
||||
if value is not None:
|
||||
return value
|
||||
if default is not None:
|
||||
return default
|
||||
raise KeyError(key)
|
||||
|
||||
|
||||
class AsyncCacheBackend(CacheBackend):
|
||||
"""
|
||||
缓存后端基类,定义通用的缓存接口(异步)
|
||||
@@ -420,7 +457,7 @@ class _MemoryTLRUCache(MemoryTLRUCache):
|
||||
self.__setting_ttls.pop(key, None)
|
||||
|
||||
|
||||
class MemoryBackend(CacheBackend):
|
||||
class MemoryBackend(AtomicCacheBackend):
|
||||
"""
|
||||
基于 `cachetools.TLRUCache` 实现的缓存后端
|
||||
"""
|
||||
@@ -481,6 +518,32 @@ class MemoryBackend(CacheBackend):
|
||||
else:
|
||||
region_cache[key] = value
|
||||
|
||||
def store(
|
||||
self,
|
||||
key: str,
|
||||
value: Any,
|
||||
ttl: Optional[int] = None,
|
||||
region: Optional[str] = DEFAULT_CACHE_REGION,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""严格写入内存缓存。"""
|
||||
self.set(key=key, value=value, ttl=ttl, region=region, **kwargs)
|
||||
|
||||
def consume(
|
||||
self,
|
||||
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:
|
||||
return None
|
||||
try:
|
||||
return region_cache.pop(key)
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def exists(self, key: str, region: Optional[str] = DEFAULT_CACHE_REGION) -> bool:
|
||||
"""
|
||||
判断缓存键是否存在
|
||||
@@ -721,7 +784,7 @@ def AsyncFileCache(
|
||||
|
||||
def Cache(cache_type: Literal['ttl', 'lru'] = 'ttl',
|
||||
maxsize: Optional[int] = None,
|
||||
ttl: Optional[int] = None) -> CacheBackend:
|
||||
ttl: Optional[int] = None) -> AtomicCacheBackend:
|
||||
"""
|
||||
根据配置获取缓存后端实例(内存或Redis),maxsize仅在未启用Redis时生效
|
||||
|
||||
@@ -1023,7 +1086,7 @@ class CacheProxy:
|
||||
缓存代理类,将缓存后端的方法直接代理到实例上
|
||||
"""
|
||||
|
||||
def __init__(self, cache_backend: CacheBackend, region: str):
|
||||
def __init__(self, cache_backend: AtomicCacheBackend, region: str):
|
||||
"""
|
||||
初始化缓存代理
|
||||
|
||||
@@ -1096,6 +1159,16 @@ class CacheProxy:
|
||||
kwargs.setdefault('region', self._region)
|
||||
self._cache_backend.set(key, value, **kwargs)
|
||||
|
||||
def store(self, key: str, value: Any, **kwargs: Any) -> None:
|
||||
"""严格写入缓存,后端故障向调用方传播。"""
|
||||
kwargs.setdefault('region', self._region)
|
||||
self._cache_backend.store(key, value, **kwargs)
|
||||
|
||||
def consume(self, key: str, **kwargs: Any) -> Any:
|
||||
"""原子领取并删除缓存值。"""
|
||||
kwargs.setdefault('region', self._region)
|
||||
return self._cache_backend.consume(key, **kwargs)
|
||||
|
||||
def delete(self, key: str, **kwargs) -> None:
|
||||
"""
|
||||
删除缓存值
|
||||
|
||||
@@ -7,11 +7,11 @@ import time
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from app.runtime.correlation import correlation_scope
|
||||
from app.runtime.event.binding import EventBindingResolver
|
||||
from app.runtime.event.registry import EventRegistry
|
||||
from app.runtime.execution import run_in_threadpool
|
||||
from app.runtime.log import logger
|
||||
from app.runtime.correlation import correlation_scope
|
||||
from app.runtime.observability import observe_duration
|
||||
from app.schemas.types import EventType
|
||||
|
||||
@@ -129,6 +129,44 @@ class EventDispatcher:
|
||||
(handler, isolated),
|
||||
)
|
||||
|
||||
def dispatch_broadcast_strict(
|
||||
self,
|
||||
event: Any,
|
||||
async_runner: Callable[[Any], Any],
|
||||
) -> None:
|
||||
"""串行执行广播处理器并等待完成,任一处理失败时向调用方抛出。"""
|
||||
handlers = self._registry.broadcast_snapshot(event.event_type)
|
||||
target_plugin_id = None
|
||||
if event.event_type == EventType.MessageAction and isinstance(
|
||||
event.event_data,
|
||||
dict,
|
||||
):
|
||||
target_plugin_id = event.event_data.get("__mp_target_plugin_id")
|
||||
for handler_id, handler in handlers:
|
||||
if not self._registry.is_handler_enabled(handler):
|
||||
continue
|
||||
if target_plugin_id and not self.should_dispatch_to_target_plugin(
|
||||
handler,
|
||||
handler_id,
|
||||
str(target_plugin_id),
|
||||
):
|
||||
continue
|
||||
if isinstance(event.event_data, dict):
|
||||
event_data = event.event_data.copy()
|
||||
event_data.pop("__mp_target_plugin_id", None)
|
||||
else:
|
||||
event_data = event.event_data
|
||||
isolated = self._event_factory(
|
||||
event_type=event.event_type,
|
||||
event_data=event_data,
|
||||
priority=event.priority,
|
||||
correlation_id=event.correlation_id,
|
||||
)
|
||||
if inspect.iscoroutinefunction(handler):
|
||||
async_runner(self.invoke_async_strict(handler, isolated))
|
||||
else:
|
||||
self.invoke_sync_strict(handler, isolated)
|
||||
|
||||
def safe_invoke_sync(self, handler: Callable, event: Any) -> None:
|
||||
"""仅在处理器启用时执行同步调用。"""
|
||||
if self._registry.is_handler_enabled(handler):
|
||||
@@ -162,6 +200,34 @@ class EventDispatcher:
|
||||
e=err,
|
||||
)
|
||||
|
||||
def invoke_sync_strict(
|
||||
self,
|
||||
handler: Callable[..., object],
|
||||
event: Any,
|
||||
) -> None:
|
||||
"""解析并执行同步处理器,记录错误后向 durable 调用方传播。"""
|
||||
resolved = self._binding_resolver.resolve(handler)
|
||||
if not resolved:
|
||||
raise RuntimeError("事件处理器实例不可用")
|
||||
method, binding, class_name, method_name = resolved
|
||||
with correlation_scope(event.correlation_id):
|
||||
try:
|
||||
with observe_duration(
|
||||
"event.handler.duration",
|
||||
event_type=event.event_type.value,
|
||||
handler_type="bound" if class_name else "function",
|
||||
):
|
||||
method(event)
|
||||
except Exception as err:
|
||||
self._error_handler(
|
||||
event=event,
|
||||
module_name=binding.owner_name,
|
||||
class_name=class_name,
|
||||
method_name=method_name,
|
||||
e=err,
|
||||
)
|
||||
raise
|
||||
|
||||
async def invoke_async(self, handler: Callable, event: Any) -> None:
|
||||
"""解析实例绑定,并按处理器类型选择协程、线程池或同步调用。"""
|
||||
resolved = self._binding_resolver.resolve(handler)
|
||||
@@ -190,6 +256,39 @@ class EventDispatcher:
|
||||
e=err,
|
||||
)
|
||||
|
||||
async def invoke_async_strict(
|
||||
self,
|
||||
handler: Callable[..., object],
|
||||
event: Any,
|
||||
) -> None:
|
||||
"""解析并等待处理器完成,记录错误后向 durable 调用方传播。"""
|
||||
resolved = self._binding_resolver.resolve(handler)
|
||||
if not resolved:
|
||||
raise RuntimeError("事件处理器实例不可用")
|
||||
method, binding, class_name, method_name = resolved
|
||||
with correlation_scope(event.correlation_id):
|
||||
try:
|
||||
with observe_duration(
|
||||
"event.handler.duration",
|
||||
event_type=event.event_type.value,
|
||||
handler_type="bound" if class_name else "function",
|
||||
):
|
||||
if inspect.iscoroutinefunction(method):
|
||||
await method(event)
|
||||
elif binding.run_sync_in_threadpool or not class_name:
|
||||
await run_in_threadpool(method, event)
|
||||
else:
|
||||
method(event)
|
||||
except Exception as err:
|
||||
self._error_handler(
|
||||
event=event,
|
||||
module_name=binding.owner_name,
|
||||
class_name=class_name,
|
||||
method_name=method_name,
|
||||
e=err,
|
||||
)
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def should_dispatch_to_target_plugin(
|
||||
handler: Callable,
|
||||
|
||||
@@ -526,6 +526,36 @@ class EventManager(metaclass=Singleton):
|
||||
logger.error(f"Unknown event type: {etype}")
|
||||
return None
|
||||
|
||||
def send_event_strict(
|
||||
self,
|
||||
etype: EventType,
|
||||
data: Optional[Union[dict[str, object], ChainEventData]] = None,
|
||||
priority: Optional[int] = DEFAULT_EVENT_PRIORITY,
|
||||
) -> Event:
|
||||
"""同步等待全部广播处理器完成,任一失败时阻止 durable 消息结算。"""
|
||||
event = Event(etype, data, priority)
|
||||
with self.__lifecycle_lock:
|
||||
if self.__lifecycle_state != "running":
|
||||
raise RuntimeError(f"事件处理处于 {self.__lifecycle_state} 状态")
|
||||
self.__dispatcher.dispatch_broadcast_strict(
|
||||
event,
|
||||
self.__wait_strict_async_handler,
|
||||
)
|
||||
return event
|
||||
|
||||
@staticmethod
|
||||
def __wait_strict_async_handler(coroutine: Any) -> Any:
|
||||
"""在主事件循环等待异步处理器,禁止循环线程同步等待自身。"""
|
||||
loop = global_vars.loop
|
||||
try:
|
||||
running_loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
running_loop = None
|
||||
if running_loop is loop:
|
||||
coroutine.close()
|
||||
raise RuntimeError("主事件循环线程不能同步等待 durable 事件处理器")
|
||||
return asyncio.run_coroutine_threadsafe(coroutine, loop).result()
|
||||
|
||||
async def async_send_event(self, etype: Union[EventType, ChainEventType],
|
||||
data: Optional[Union[Dict, ChainEventData]] = None,
|
||||
priority: Optional[int] = DEFAULT_EVENT_PRIORITY) -> Optional[Event]:
|
||||
|
||||
Reference in New Issue
Block a user