mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-09 15:19:54 +08:00
Run `ruff check app tests scripts --fix` with the configured rule set (E4/E7/E9/F/I): import sorting (I001), f-strings without placeholders (F541), lambda/variable/unused-import cleanups that ruff marks safe. Nine files whose only ruff debt sits next to pre-existing pylint import-resolution errors (lazy exports, sys.path script imports) or whose "unused" imports are reached through attribute access by tests are left untouched so the changed-file pylint gate and the benchmark tests stay green; their diagnostics remain in the baseline. app/startup/lifecycle/__init__.py and app/main.py import business modules after cache composition, import-path and stdio setup on purpose (the source comments document the contract), so declare E402 per-file-ignores for them instead of adding per-line noqa. Ruff ratchet watermark drops from 498 to 100; sync the baseline fixture and the optimization checklist figure. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
94 lines
3.5 KiB
Python
94 lines
3.5 KiB
Python
"""事件处理异常的通知、降级和递归保护策略。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import threading
|
||
import traceback
|
||
from collections import OrderedDict
|
||
from collections.abc import Callable
|
||
from typing import Any, Optional
|
||
|
||
from app.runtime.log import logger
|
||
from app.schemas.types import EventType
|
||
|
||
EventErrorNotifier = Callable[[str, str], object]
|
||
_MAX_REPORTED_ERRORS = 4096
|
||
|
||
|
||
class EventErrorPolicy:
|
||
"""隔离处理器异常,并阻止 SystemError 处理失败再次广播。"""
|
||
|
||
def __init__(
|
||
self,
|
||
*,
|
||
notifier: Callable[[], Optional[EventErrorNotifier]],
|
||
emit_system_error: Callable[[dict[str, Any]], object],
|
||
) -> None:
|
||
"""注入通知与错误广播回调,并初始化本进程有界提示去重记录。"""
|
||
self._notifier = notifier
|
||
self._emit_system_error = emit_system_error
|
||
self._reported_errors: OrderedDict[tuple[str, ...], None] = OrderedDict()
|
||
self._reported_errors_lock = threading.Lock()
|
||
|
||
def _is_repeated_error(self, event: Any, handler: str, error: Exception) -> bool:
|
||
"""按持久事件及处理器错误去重提示;普通事件保持原行为,缓存有界。"""
|
||
payload = event.event_data
|
||
event_key = payload.get("idempotency_key") if isinstance(payload, dict) else None
|
||
if not isinstance(event_key, str) or not event_key:
|
||
return False
|
||
key = (event.event_type.value, event_key, handler, str(error))
|
||
with self._reported_errors_lock:
|
||
if key in self._reported_errors:
|
||
self._reported_errors.move_to_end(key)
|
||
return True
|
||
self._reported_errors[key] = None
|
||
if len(self._reported_errors) > _MAX_REPORTED_ERRORS:
|
||
self._reported_errors.popitem(last=False)
|
||
return False
|
||
|
||
def handle(
|
||
self,
|
||
*,
|
||
event: Any,
|
||
module_name: str,
|
||
class_name: str,
|
||
method_name: str,
|
||
error: Exception,
|
||
) -> None:
|
||
"""每次失败保留日志,仅对同一持久事件的相同错误提示一次。
|
||
|
||
Outbox 仍按严格回执重试和结算;去重只作用于本进程的系统提示及错误广播,
|
||
不吞掉原处理器异常,也不把未完成的持久事件误标为完成。
|
||
"""
|
||
trace = traceback.format_exc()
|
||
logger.error("%s 事件处理出错:%s - %s", module_name, str(error), trace)
|
||
if self._is_repeated_error(
|
||
event, f"{module_name}.{class_name}.{method_name}", error
|
||
):
|
||
return
|
||
notifier = self._notifier()
|
||
if notifier:
|
||
try:
|
||
notifier(
|
||
f"{module_name} 处理事件 {event.event_type} 时出错",
|
||
f"{class_name}.{method_name}:{str(error)}",
|
||
)
|
||
except Exception as notify_error:
|
||
logger.error("发送事件错误通知失败:%s", str(notify_error))
|
||
if event.event_type == EventType.SystemError:
|
||
logger.error(
|
||
"SystemError 处理器再次失败,停止错误事件递归广播:%s.%s",
|
||
class_name,
|
||
method_name,
|
||
)
|
||
return
|
||
self._emit_system_error(
|
||
{
|
||
"type": "event",
|
||
"event_type": event.event_type,
|
||
"event_handle": f"{class_name}.{method_name}",
|
||
"error": str(error),
|
||
"traceback": trace,
|
||
}
|
||
)
|