refactor: close transactional boundary debt batch

This commit is contained in:
jxxghp
2026-08-28 10:36:12 +08:00
parent 3f8d5990e7
commit aa8751f775
105 changed files with 6507 additions and 1691 deletions
+80 -7
View File
@@ -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:
"""
删除缓存值
+100 -1
View File
@@ -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,
+30
View File
@@ -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]: