From 71dc9df7ffc206e2e4ce82dea9acd1556341a3d8 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Wed, 20 May 2026 09:38:37 +0800 Subject: [PATCH] fix: ignore expected module rate limits --- app/chain/__init__.py | 29 +++++++++++ tests/test_chain_rate_limit.py | 95 ++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 tests/test_chain_rate_limit.py diff --git a/app/chain/__init__.py b/app/chain/__init__.py index 68d150d2..1bdf49a7 100644 --- a/app/chain/__init__.py +++ b/app/chain/__init__.py @@ -26,6 +26,7 @@ from app.helper.message import MessageHelper, MessageQueueManager, MessageTempla from app.helper.service import ServiceConfigHelper from app.log import logger from app.schemas import ( + RateLimitExceededException, TransferInfo, TransferTorrent, ExistMediaInfo, @@ -204,6 +205,18 @@ class ChainBase(metaclass=ABCMeta): }, ) + @staticmethod + def __handle_rate_limit_error( + err: RateLimitExceededException, source_type: str, source_id: str, + method: str, **kwargs + ) -> None: + """ + 处理本地限流跳过,避免预期的限流状态进入系统错误告警。 + """ + if kwargs.get("raise_exception"): + raise err + logger.info(f"{source_type} {source_id}.{method} 已限流,跳过执行:{str(err)}") + def __execute_plugin_modules( self, method: str, result: Any, *args, **kwargs ) -> Any: @@ -227,6 +240,10 @@ class ChainBase(metaclass=ABCMeta): result.extend(temp) else: break + except RateLimitExceededException as err: + self.__handle_rate_limit_error( + err, "插件", plugin_id, method, **kwargs + ) except Exception as err: self.__handle_plugin_error( err, plugin_id, plugin_name, method, **kwargs @@ -264,6 +281,10 @@ class ChainBase(metaclass=ABCMeta): result.extend(temp) else: break + except RateLimitExceededException as err: + self.__handle_rate_limit_error( + err, "插件", plugin_id, method, **kwargs + ) except Exception as err: self.__handle_plugin_error( err, plugin_id, plugin_name, method, **kwargs @@ -303,6 +324,10 @@ class ChainBase(metaclass=ABCMeta): else: # 中止继续执行 break + except RateLimitExceededException as err: + self.__handle_rate_limit_error( + err, "模块", module_id, method, **kwargs + ) except Exception as err: logger.error(traceback.format_exc()) self.__handle_system_error( @@ -353,6 +378,10 @@ class ChainBase(metaclass=ABCMeta): else: # 中止继续执行 break + except RateLimitExceededException as err: + self.__handle_rate_limit_error( + err, "模块", module_id, method, **kwargs + ) except Exception as err: logger.error(traceback.format_exc()) self.__handle_system_error( diff --git a/tests/test_chain_rate_limit.py b/tests/test_chain_rate_limit.py new file mode 100644 index 00000000..f644f4fc --- /dev/null +++ b/tests/test_chain_rate_limit.py @@ -0,0 +1,95 @@ +import asyncio +import sys +import unittest +from types import ModuleType +from unittest.mock import Mock + +sys.modules.setdefault("qbittorrentapi", ModuleType("qbittorrentapi")) +setattr(sys.modules["qbittorrentapi"], "TorrentFilesList", list) +sys.modules.setdefault("transmission_rpc", ModuleType("transmission_rpc")) +setattr(sys.modules["transmission_rpc"], "File", object) + +from app.chain import ChainBase +from app.schemas import RateLimitExceededException + + +class _LimitedModule: + def get_name(self): + """ + 返回测试模块名称。 + """ + return "限流测试模块" + + def get_priority(self): + """ + 返回测试模块优先级。 + """ + return 1 + + def limited_method(self, raise_exception: bool = False): + """ + 模拟同步模块在本地限流期间跳过调用。 + """ + raise RateLimitExceededException("[limited_method] 限流期间,跳过调用") + + async def async_limited_method(self, raise_exception: bool = False): + """ + 模拟异步模块在本地限流期间跳过调用。 + """ + raise RateLimitExceededException("[async_limited_method] 限流期间,跳过调用") + + +class ChainRateLimitTest(unittest.TestCase): + def _build_chain(self): + """ + 构造隔离的 ChainBase,避免依赖真实模块和插件运行状态。 + """ + chain = ChainBase() + limited_module = _LimitedModule() + chain.pluginmanager = Mock() + chain.pluginmanager.get_plugin_modules.return_value = {} + chain.modulemanager = Mock() + chain.modulemanager.get_running_modules.return_value = [limited_module] + chain.messagehelper = Mock() + chain.eventmanager = Mock() + return chain + + def test_rate_limit_is_not_reported_as_system_error(self): + """ + 本地限流跳过不应写入系统错误通知或事件。 + """ + chain = self._build_chain() + + result = chain.run_module("limited_method") + + self.assertIsNone(result) + chain.messagehelper.put.assert_not_called() + chain.eventmanager.send_event.assert_not_called() + + def test_rate_limit_can_still_be_raised_explicitly(self): + """ + 调用方显式要求抛出异常时,限流异常应继续向上抛出。 + """ + chain = self._build_chain() + + with self.assertRaises(RateLimitExceededException): + chain.run_module("limited_method", raise_exception=True) + + chain.messagehelper.put.assert_not_called() + chain.eventmanager.send_event.assert_not_called() + + def test_async_rate_limit_is_not_reported_as_system_error(self): + """ + 异步模块的本地限流跳过也不应触发系统错误路径。 + """ + chain = self._build_chain() + + result = asyncio.run(chain.async_run_module("async_limited_method")) + + self.assertIsNone(result) + chain.messagehelper.put.assert_not_called() + chain.eventmanager.send_event.assert_not_called() + + +if __name__ == "__main__": + unittest.main()