From e8f8610c5bcfffceafaf051823ddaf9079668cd3 Mon Sep 17 00:00:00 2001 From: fullsend-code <278716306+fullsend-ai-coder[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:05:26 +0000 Subject: [PATCH] feat(#3164): add retry on 5xx for Testing Farm API calls Replace the plain integer max_retries=5 on the HTTPAdapter with a proper urllib3 Retry strategy that retries on HTTP 500, 502, 503, and 504 status codes with exponential backoff (backoff_factor=1). The previous configuration only retried transport-level connection errors, not server-side HTTP error responses. The retry strategy explicitly allows GET, POST, and DELETE methods so that TF request submission and cancellation are also retried. Closes #3164 --- .../worker/helpers/testing_farm_client.py | 12 +++++- tests/unit/test_testing_farm.py | 43 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/packit_service/worker/helpers/testing_farm_client.py b/packit_service/worker/helpers/testing_farm_client.py index cca204e95..61cddd56b 100644 --- a/packit_service/worker/helpers/testing_farm_client.py +++ b/packit_service/worker/helpers/testing_farm_client.py @@ -11,6 +11,7 @@ from ogr.utils import RequestResponse from packit.constants import HTTP_REQUEST_TIMEOUT from packit.exceptions import PackitException +from urllib3.util.retry import Retry from packit_service.config import ServiceConfig from packit_service.constants import ( @@ -33,7 +34,16 @@ def __init__(self, api_url: str, token: str, use_internal_tf: bool = False) -> N self._token = token self.session = requests.session() - self.session.mount("https://", requests.adapters.HTTPAdapter(max_retries=5)) + retry_strategy = Retry( + total=5, + backoff_factor=1, + status_forcelist=[500, 502, 503, 504], + allowed_methods=["GET", "POST", "DELETE"], + ) + self.session.mount( + "https://", + requests.adapters.HTTPAdapter(max_retries=retry_strategy), + ) self.session.headers.update({"Authorization": f"Bearer {self._token}"}) @property diff --git a/tests/unit/test_testing_farm.py b/tests/unit/test_testing_farm.py index ec3610e4a..d59b69b0c 100644 --- a/tests/unit/test_testing_farm.py +++ b/tests/unit/test_testing_farm.py @@ -2480,3 +2480,46 @@ def test_empty_development_list(self): {"fedora-development": []}, ) assert TFJobHelper.is_freshly_branched_fedora("fedora-42") is False + + +class TestTestingFarmClientRetry: + """Tests for the retry mechanism configured on the TF API client.""" + + def test_retry_strategy_is_configured(self): + """Verify that the session retry strategy retries on 5xx status codes.""" + from urllib3.util.retry import Retry + + service_config = ServiceConfig.get_service_config() + client = TFClient( + api_url=service_config.testing_farm_api_url, + token=service_config.testing_farm_secret, + ) + adapter = client.session.get_adapter("https://example.com") + retry = adapter.max_retries + + assert isinstance(retry, Retry) + assert retry.total == 5 + assert retry.backoff_factor == 1 + assert 500 in retry.status_forcelist + assert 502 in retry.status_forcelist + assert 503 in retry.status_forcelist + assert 504 in retry.status_forcelist + + def test_retry_allows_post_and_delete(self): + """Verify that POST and DELETE methods are retried (needed for TF + request submission and cancellation).""" + from urllib3.util.retry import Retry + + service_config = ServiceConfig.get_service_config() + client = TFClient( + api_url=service_config.testing_farm_api_url, + token=service_config.testing_farm_secret, + ) + adapter = client.session.get_adapter("https://example.com") + retry = adapter.max_retries + + assert isinstance(retry, Retry) + allowed = retry.allowed_methods + assert "GET" in allowed + assert "POST" in allowed + assert "DELETE" in allowed