mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 07:27:15 +08:00
fix(monitor,transfer): 修复 FUSE 挂载无响应导致的监控冻死、整理链锁死与漏件 (#6276)
* wip(v3): 移植监控与整理韧性修复到 v3 基线 包含:监控看门狗隔离/挂载探测、整理队列持久化、文件系统子进程代理、 写入原子化。迁移重挂到 v3 链 8a4c7e1d2f90 -> 7f5c1d2e3a4b -> e3d9f4b7c806。 tmdb 相关测试尚未通过,待定位。 * fix(v3): 修正移植引入的 16 项测试失败 - poller.py:合并时我方保留的行仍用旧变量名 merged_snapshot,而 v3 已统一 改名为 current_snapshot,导致 NameError 被外层 except 吞掉、快照从未保存 - smb.py:采纳 f-string 拆分写法,恢复 Python 3.11 可解析 - dispatcher 测试:历史查重由 _should_skip_by_history 统一承担,mock 点随之调整 - tmdb 缓存测试:补充 v3 新增的 media_source/media_id 字段 - tmdb 重试测试:为 fake 补充 match_multi/async_match_multi 尚余 3 项与 v3 识别流程的连接失败处理有关,待单独判断。 * fix(v3): 测试适配 v3 的 media_source/media_id 重构 v3 将媒体标识从 tmdbid 统一重构为 media_source + media_id,recognize_media 的 tmdbid 参数已被 **kwargs 静默吞掉——传了也不生效,流程会误降级到名称搜索。 tmdb 重试用例改用新参数后恢复正确路径。 同时修正 fake 的 match_multi 语义:真实实现(tmdbapi.match_multi)吞掉所有 异常并返回 None,连接失败与「未找到」在该路径上本就不可区分,fake 需保持一致。 至此移植引入的 19 项失败全部清零。 --------- Co-authored-by: Aqr-K <Aqr-K@users.noreply.github.com>
This commit is contained in:
@@ -1,2 +1,16 @@
|
||||
class TMDbException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class TMDbConnectionError(TMDbException):
|
||||
"""
|
||||
TMDB连接失败异常。
|
||||
|
||||
仅在确认为传输层/响应格式问题(如底层HTTP请求失败、响应无法解析为JSON)时抛出,
|
||||
与TMDB业务层明确返回的错误(如404条目不存在、参数错误等,仍抛出普通TMDbException)
|
||||
区分开,便于上层区分"网络故障,请重试"与"条目确实不存在"两类完全不同的处理与文案。
|
||||
|
||||
继承自TMDbException,因此现有 `except TMDbException` 代码路径无需修改即可
|
||||
继续捕获本异常,保持向后兼容。
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -12,10 +12,30 @@ import requests.exceptions
|
||||
from app.core.cache import cached, fresh, async_fresh
|
||||
from app.core.config import settings
|
||||
from app.utils.http import RequestUtils, AsyncRequestUtils
|
||||
from .exceptions import TMDbException
|
||||
from .exceptions import TMDbException, TMDbConnectionError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 单次重试前的退避等待时间(秒)。NAS+FUSE网盘等环境下TMDB连接的失败大多是数秒内
|
||||
# 可自愈的瞬时抖动,零间隔重试(或异步完全不重试)基本无法穿越这类抖动窗口;
|
||||
# 识别链路是同步阻塞调用,1-3秒的等待可接受,超出3秒则会让识别耗时明显变长,
|
||||
# 故取区间内的经验值。
|
||||
RETRY_BACKOFF_SECONDS = 2
|
||||
|
||||
|
||||
def _is_business_failure_snapshot(snapshot) -> bool:
|
||||
"""
|
||||
判断响应快照是否为TMDB业务失败(success=false,如404/限流/服务端错误的合法JSON)。
|
||||
|
||||
这类响应若入缓存会把瞬时失败固化整个TTL周期(如12小时),期间同key请求
|
||||
直接命中失败快照;跳过缓存让下次请求重新确认。真正的负缓存(条目确认
|
||||
不存在)由上层 TmdbCache 负责,request 层不做失败记忆。
|
||||
"""
|
||||
if not isinstance(snapshot, dict):
|
||||
return False
|
||||
json_data = snapshot.get("json")
|
||||
return isinstance(json_data, dict) and json_data.get("success") is False
|
||||
|
||||
|
||||
class TMDb(object):
|
||||
_RESPONSE_SNAPSHOT_MARKER = "__mp_tmdb_response_snapshot__"
|
||||
@@ -136,15 +156,21 @@ class TMDb(object):
|
||||
def wait_on_rate_limit(self, wait_on_rate_limit):
|
||||
self._wait_on_rate_limit = bool(wait_on_rate_limit)
|
||||
|
||||
@cached(maxsize=settings.CONF.tmdb, ttl=settings.CONF.meta, skip_none=True)
|
||||
@cached(maxsize=settings.CONF.tmdb, ttl=settings.CONF.meta, skip_none=True,
|
||||
skip_if=_is_business_failure_snapshot)
|
||||
def request(self, method, url, data, json, **kwargs):
|
||||
req = self._request_once(method, url, data, json)
|
||||
if req is None and method == "GET" and self._owns_session:
|
||||
logger.debug("TMDB同步请求失败,重建会话后重试一次")
|
||||
logger.debug(f"TMDB同步请求失败,等待{RETRY_BACKOFF_SECONDS}秒后重建会话重试一次")
|
||||
# 同步阻塞识别链线程;1-3秒的退避等待可接受,能显著提升对瞬时抖动的容错,
|
||||
# 详见模块级常量 RETRY_BACKOFF_SECONDS 的说明。
|
||||
time.sleep(RETRY_BACKOFF_SECONDS)
|
||||
self._reset_owned_session()
|
||||
req = self._request_once(method, url, data, json)
|
||||
if req is None:
|
||||
raise TMDbException("无法连接TheMovieDb,请检查网络连接!")
|
||||
# 抛出更具体的连接异常子类,供上层(如TMDB详情查询)区分"网络故障"
|
||||
# 与"TMDB业务层明确返回的错误"(如404条目不存在),两者不能混为一谈。
|
||||
raise TMDbConnectionError("无法连接TheMovieDb,请检查网络连接!")
|
||||
return self._snapshot_response(req)
|
||||
|
||||
def _request_once(self, method, url, data, json):
|
||||
@@ -155,16 +181,28 @@ class TMDb(object):
|
||||
return self._req.get_res(url, params=data, json=json)
|
||||
return self._req.post_res(url, data=data, json=json)
|
||||
|
||||
@cached(maxsize=settings.CONF.tmdb, ttl=settings.CONF.meta, skip_none=True)
|
||||
@cached(maxsize=settings.CONF.tmdb, ttl=settings.CONF.meta, skip_none=True,
|
||||
skip_if=_is_business_failure_snapshot)
|
||||
async def async_request(self, method, url, data, json, **kwargs):
|
||||
if method == "GET":
|
||||
req = await self._async_req.get_res(url, params=data, json=json)
|
||||
else:
|
||||
req = await self._async_req.post_res(url, data=data, json=json)
|
||||
req = await self._async_request_once(method, url, data, json)
|
||||
if req is None:
|
||||
raise TMDbException("无法连接TheMovieDb,请检查网络连接!")
|
||||
logger.debug(f"TMDB异步请求失败,等待{RETRY_BACKOFF_SECONDS}秒后重试一次")
|
||||
# 异步会话(AsyncRequestUtils)不像同步会话那样支持按需重建,
|
||||
# 这里退化为原会话上的纯重试,同样以退避等待应对瞬时抖动。
|
||||
await asyncio.sleep(RETRY_BACKOFF_SECONDS)
|
||||
req = await self._async_request_once(method, url, data, json)
|
||||
if req is None:
|
||||
raise TMDbConnectionError("无法连接TheMovieDb,请检查网络连接!")
|
||||
return self._snapshot_response(req)
|
||||
|
||||
async def _async_request_once(self, method, url, data, json):
|
||||
"""
|
||||
执行一次TMDB异步请求,调用方负责决定是否重试。
|
||||
"""
|
||||
if method == "GET":
|
||||
return await self._async_req.get_res(url, params=data, json=json)
|
||||
return await self._async_req.post_res(url, data=data, json=json)
|
||||
|
||||
@classmethod
|
||||
def _snapshot_response(cls, response):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user