diff --git a/qBitrr/arss/arr_base.py b/qBitrr/arss/arr_base.py index f3b090ef..e47f59d2 100644 --- a/qBitrr/arss/arr_base.py +++ b/qBitrr/arss/arr_base.py @@ -3406,6 +3406,8 @@ def run_search_loop(self) -> NoReturn: loop_timer = timedelta(minutes=15) timer = datetime.now() years_index = 0 + years: list[int] = [] + years_count = 0 totcommands = -1 self.db_update_processed = False event = self.manager.qbit_manager.shutdown_event @@ -3416,10 +3418,21 @@ def run_search_loop(self) -> NoReturn: years_index = 0 totcommands = -1 timer = datetime.now() - years: list[int] = [] - years_count = 0 - if self.search_by_year: - years, years_count = self.get_year_search() + if self.search_by_year and years_index == 0: + try: + years, years_count = self.get_year_search() + except PyarrConnectionError as e: + self.logger.warning( + "Could not reach %s Arr API during search loop: %s", + self._name, + e, + ) + self._handle_delay_loop_exception( + DelayLoopException(length=300, error_type="arr"), + event.wait, + reset_torrent_scan_delay=True, + ) + continue if self.search_by_year and years: totcommands = -1 if years_index == 0: diff --git a/tests/test_search_and_db_query_hooks.py b/tests/test_search_and_db_query_hooks.py index fc451009..43d3f0cc 100644 --- a/tests/test_search_and_db_query_hooks.py +++ b/tests/test_search_and_db_query_hooks.py @@ -124,6 +124,48 @@ def test_sonarr_series_search_smart_uses_episodes(self) -> None: self.assertIs(rows[0][0], episode) +class TestSearchLoopYearLoading(unittest.TestCase): + def test_arr_outage_during_year_loading_does_not_kill_worker(self) -> None: + """Regression: an Arr outage while loading years must back off inside the worker.""" + from qBitrr.arss.arr_base import ArrBase + from qBitrr.arss.arr_shared import PyarrConnectionError + + arr = ArrBase.__new__(ArrBase) + arr._name = "Radarr.Test" + arr.logger = MagicMock() + arr.search_missing = True + arr.do_upgrade_search = False + arr.quality_unmet_search = False + arr.custom_format_unmet_search = False + arr.ombi_search_requests = False + arr.overseerr_requests = False + arr.search_by_year = True + arr.loop_completed = False + arr.manager = MagicMock() + event = MagicMock() + event.is_set.side_effect = [False, True] + arr.manager.qbit_manager.shutdown_event = event + + with ( + patch("qBitrr.arss.arr_base.run_logs"), + patch.object(arr, "_sync_loop_settings_from_config"), + patch.object( + arr, + "get_year_search", + side_effect=PyarrConnectionError("Arr unavailable"), + ), + patch.object(arr, "_handle_delay_loop_exception") as delay_handler, + ): + arr.run_search_loop() + + delay_handler.assert_called_once() + delay_exc = delay_handler.call_args.args[0] + self.assertEqual(delay_exc.error_type, "arr") + self.assertEqual(delay_exc.length, 300) + self.assertTrue(delay_handler.call_args.kwargs["reset_torrent_scan_delay"]) + arr.logger.critical.assert_not_called() + + class TestPreserveVsLiveClassification(unittest.TestCase): def test_uri_is_preserve_not_live(self) -> None: from qBitrr.config_reload_policy import ReloadCategory, classify_config_key