diff --git a/app/chain/subscribe.py b/app/chain/subscribe.py index 818c01ff7..da41f1bf9 100644 --- a/app/chain/subscribe.py +++ b/app/chain/subscribe.py @@ -1375,7 +1375,13 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase): ): logger.debug(f"search lock acquired at {datetime.now()}") else: - logger.warn("search上锁超时") + logger.error("订阅搜索锁等待超时,已中止本轮执行") + if progress_callback: + progress_callback( + value=100, + text="订阅搜索锁等待超时,已跳过本轮", + ) + return subscribeoper = SubscribeOper() if sid: @@ -1819,7 +1825,13 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase): ): logger.debug(f"match lock acquired at {datetime.now()}") else: - logger.warn("match上锁超时") + logger.error("订阅匹配锁等待超时,已中止本轮执行") + if progress_callback: + progress_callback( + value=100, + text="订阅匹配锁等待超时,已跳过本轮", + ) + return # 预识别所有未识别的种子 processed_torrents: Dict[str, List[Context]] = {} diff --git a/app/locales/en-US.json b/app/locales/en-US.json index e2c96552f..cc1a2209a 100644 --- a/app/locales/en-US.json +++ b/app/locales/en-US.json @@ -309,6 +309,8 @@ "微信 ClawBot 通知未启用或配置尚未保存,请先保存并启用当前渠道": "WeChat ClawBot notification is not enabled or the configuration has not been saved. Please save and enable this channel first", "请输入至少一个有效的站点 ID": "Enter at least one valid site ID", "所有订阅搜索完成": "All subscription searches are complete", + "订阅搜索锁等待超时,已跳过本轮": "Subscription search lock timed out, this round was skipped", + "订阅匹配锁等待超时,已跳过本轮": "Subscription matching lock timed out, this round was skipped", "请输入订阅 ID,多个 ID 用空格分隔,或输入 all": "Enter subscription IDs separated by spaces, or enter all", "请输入至少一个有效的订阅 ID": "Enter at least one valid subscription ID", "格式错误,请输入:cookie [2fa_code/secret]": "Invalid format. Enter: cookie [2fa_code/secret]", diff --git a/tests/test_subscribe_search_state.py b/tests/test_subscribe_search_state.py index b03330748..272cf94d0 100644 --- a/tests/test_subscribe_search_state.py +++ b/tests/test_subscribe_search_state.py @@ -34,6 +34,18 @@ class _SubscribeOper: self.updates.append((sid, payload)) +class _TimedOutLock: + """模拟订阅搜索锁在等待窗口内始终无法取得。""" + + def acquire(self, **_kwargs): + """返回未取得锁,验证调用方不会越过互斥边界继续执行。""" + return False + + def release(self): + """超时路径不应释放未持有的锁。""" + raise AssertionError("未持有的订阅锁不应被释放") + + def _new_subscribe(created_at: datetime) -> SimpleNamespace: """ 构造一个新建电影订阅。 @@ -90,3 +102,31 @@ def test_new_subscribe_search_marks_state_after_attempt(monkeypatch) -> None: media_chain.recognize_media.assert_called_once() assert _SubscribeOper.updates == [(31, {"state": "R"})] + + +def test_subscribe_search_aborts_when_lock_times_out(monkeypatch) -> None: + """订阅搜索锁超时后必须中止,不能在无锁状态下继续访问订阅。""" + monkeypatch.setattr(SubscribeChain, "_rlock", _TimedOutLock()) + subscribe_oper = Mock() + monkeypatch.setattr(subscribe_module, "SubscribeOper", subscribe_oper) + progress = Mock() + + chain = object.__new__(SubscribeChain) + chain.search(state="N", progress_callback=progress) + + subscribe_oper.assert_not_called() + progress.assert_called_once_with( + value=100, + text="订阅搜索锁等待超时,已跳过本轮", + ) + + +def test_subscribe_match_aborts_when_lock_times_out(monkeypatch) -> None: + """订阅匹配锁超时后必须中止,不能绕过防重复下载边界。""" + monkeypatch.setattr(SubscribeChain, "_rlock", _TimedOutLock()) + progress = Mock() + + chain = object.__new__(SubscribeChain) + chain.match({"example.org": []}, progress_callback=progress) + + progress.assert_any_call(value=100, text="订阅匹配锁等待超时,已跳过本轮")