diff --git a/swh/scheduler/backend.py b/swh/scheduler/backend.py --- a/swh/scheduler/backend.py +++ b/swh/scheduler/backend.py @@ -317,6 +317,7 @@ policy: str, timestamp: Optional[datetime.datetime] = None, scheduled_cooldown: Optional[datetime.timedelta] = datetime.timedelta(days=7), + failed_cooldown: Optional[datetime.timedelta] = datetime.timedelta(days=14), db=None, cur=None, ) -> List[ListedOrigin]: @@ -354,6 +355,15 @@ query_args.append(timestamp) query_args.append(scheduled_cooldown) + if failed_cooldown: + # Don't retry failed origins too often + where_clauses.append( + "origin_visit_stats.last_failed is null " + "or origin_visit_stats.last_failed < %s - %s" + ) + query_args.append(timestamp) + query_args.append(failed_cooldown) + if policy == "oldest_scheduled_first": order_by = "origin_visit_stats.last_scheduled NULLS FIRST" elif policy == "never_visited_oldest_update_first": diff --git a/swh/scheduler/interface.py b/swh/scheduler/interface.py --- a/swh/scheduler/interface.py +++ b/swh/scheduler/interface.py @@ -390,6 +390,7 @@ policy: str, timestamp: Optional[datetime.datetime] = None, scheduled_cooldown: Optional[datetime.timedelta] = datetime.timedelta(days=7), + failed_cooldown: Optional[datetime.timedelta] = datetime.timedelta(days=14), ) -> List[ListedOrigin]: """Get at most the `count` next origins that need to be visited with the `visit_type` loader according to the given scheduling `policy`. @@ -405,6 +406,8 @@ being scheduled (defaults to the current time) scheduled_cooldown: the minimal interval before which we can schedule the same origin again + failed_cooldown: the minimal interval before which we can reschedule a + failed origin """ ... diff --git a/swh/scheduler/tests/test_scheduler.py b/swh/scheduler/tests/test_scheduler.py --- a/swh/scheduler/tests/test_scheduler.py +++ b/swh/scheduler/tests/test_scheduler.py @@ -899,6 +899,62 @@ expected ), "grab_next_visits didn't reschedule visits after the configured cooldown" + @pytest.mark.parametrize("cooldown", (7, 15)) + def test_grab_next_visits_oldest_scheduled_first_failed_cooldown( + self, swh_scheduler, listed_origins_by_type, cooldown + ): + visit_type, origins, expected = self._prepare_oldest_scheduled_first_origins( + swh_scheduler, listed_origins_by_type + ) + after = self._check_grab_next_visit_basic( + swh_scheduler, + visit_type=visit_type, + policy="oldest_scheduled_first", + expected=expected, + ) + + # Mark all visits as failed on the `after` timestamp + visit_stats = [ + OriginVisitStats( + url=origin.url, + visit_type=origin.visit_type, + last_snapshot=None, + last_eventful=None, + last_uneventful=None, + last_failed=after, + last_notfound=None, + last_scheduled=None, + ) + for i, origin in enumerate(origins) + ] + swh_scheduler.origin_visit_stats_upsert(visit_stats) + + cooldown_td = datetime.timedelta(days=cooldown) + + ret = swh_scheduler.grab_next_visits( + visit_type=visit_type, + count=len(expected) + 1, + policy="oldest_scheduled_first", + timestamp=after + cooldown_td - datetime.timedelta(seconds=1), + scheduled_cooldown=None, + failed_cooldown=cooldown_td, + ) + + assert ret == [], "Failed cooldown ignored" + + ret = swh_scheduler.grab_next_visits( + visit_type=visit_type, + count=len(expected) + 1, + policy="oldest_scheduled_first", + timestamp=after + cooldown_td + datetime.timedelta(seconds=1), + scheduled_cooldown=None, + failed_cooldown=cooldown_td, + ) + + assert sorted(ret) == sorted( + expected + ), "grab_next_visits didn't reschedule visits after the configured cooldown" + def test_grab_next_visits_never_visited_oldest_update_first( self, swh_scheduler, listed_origins_by_type, ):