Skip to content

Commit 040bcd3

Browse files
committed
fix: Add retry logic to GitHub and GitLab tag providers
Add application-level retry logic to GitHubTagProvider and GitLabTagProvider to handle transient API failures, particularly common with gitlab.com. The implementation uses the existing retry_on_exception decorator with 5 attempts and exponential backoff. Changes: - Apply @retry_on_exception decorator to GitHubTagProvider._find_tags() - Apply @retry_on_exception decorator to GitLabTagProvider._find_tags() - Add HTTP 408 (Request Timeout) to retry status codes - Remove redundant try-except blocks that immediately re-raised - Update documentation to reflect 408 status code handling The two-layer retry strategy (HTTP-level + application-level) significantly improves build reliability when GitHub/GitLab APIs experience intermittent failures. Fixes: #810 Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Ioannis Angelakopoulos <iangelak@redhat.com>
1 parent 74b0af4 commit 040bcd3

4 files changed

Lines changed: 21 additions & 24 deletions

File tree

docs/http-retry.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ The retry mechanism specifically handles these error conditions:
4343

4444
### Server Errors
4545

46+
- `408 Request Timeout` - Request took too long to complete
4647
- `504 Gateway Timeout` - Server overwhelmed or upstream timeout
4748
- `502 Bad Gateway` - Proxy/gateway errors
4849
- `503 Service Unavailable` - Temporary server overload

src/fromager/http_retry.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
This module provides a robust retry mechanism for HTTP requests with exponential
44
backoff, jitter, and specific handling for common network failures including:
5+
- Request timeouts (408)
56
- Server timeouts (502, 503, 504)
67
- Rate limiting (429, GitHub API rate limits)
78
- Connection errors and incomplete reads
@@ -35,7 +36,7 @@
3536
DEFAULT_RETRY_CONFIG = {
3637
"total": 5,
3738
"backoff_factor": 1.0,
38-
"status_forcelist": [429, 500, 502, 503, 504],
39+
"status_forcelist": [408, 429, 500, 502, 503, 504],
3940
"allowed_methods": ["GET", "PUT", "POST", "HEAD", "OPTIONS"],
4041
"raise_on_status": False,
4142
}

src/fromager/request_session.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
FROMAGER_RETRY_CONFIG = {
77
"total": int(os.environ.get("FROMAGER_HTTP_RETRIES", "8")),
88
"backoff_factor": float(os.environ.get("FROMAGER_HTTP_BACKOFF_FACTOR", "1.5")),
9-
"status_forcelist": [429, 500, 502, 503, 504],
9+
"status_forcelist": [408, 429, 500, 502, 503, 504],
1010
"allowed_methods": ["GET", "PUT", "POST", "HEAD", "OPTIONS"],
1111
"raise_on_status": False,
1212
}

src/fromager/resolver.py

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from .candidate import Candidate
3434
from .constraints import Constraints
3535
from .extras_provider import ExtrasProvider
36+
from .http_retry import RETRYABLE_EXCEPTIONS, retry_on_exception
3637
from .request_session import session
3738
from .requirements_file import RequirementType
3839

@@ -760,6 +761,12 @@ def __init__(
760761
def cache_key(self) -> str:
761762
return f"{self.organization}/{self.repo}"
762763

764+
@retry_on_exception(
765+
exceptions=RETRYABLE_EXCEPTIONS,
766+
max_attempts=5,
767+
backoff_factor=1.5,
768+
max_backoff=120.0,
769+
)
763770
def _find_tags(
764771
self,
765772
identifier: str,
@@ -773,17 +780,8 @@ def _find_tags(
773780

774781
nexturl = self.api_url.format(self=self)
775782
while nexturl:
776-
try:
777-
resp = session.get(nexturl, headers=headers)
778-
resp.raise_for_status()
779-
except Exception as e:
780-
logger.error(
781-
"%s: Failed to fetch GitHub tags from %s: %s",
782-
identifier,
783-
nexturl,
784-
e,
785-
)
786-
raise
783+
resp = session.get(nexturl, headers=headers)
784+
resp.raise_for_status()
787785

788786
for entry in resp.json():
789787
name = entry["name"]
@@ -834,23 +832,20 @@ def __init__(
834832
def cache_key(self) -> str:
835833
return f"{self.server_url}/{self.project_path}"
836834

835+
@retry_on_exception(
836+
exceptions=RETRYABLE_EXCEPTIONS,
837+
max_attempts=5,
838+
backoff_factor=1.5,
839+
max_backoff=120.0,
840+
)
837841
def _find_tags(
838842
self,
839843
identifier: str,
840844
) -> Iterable[tuple[str, Version]]:
841845
nexturl: str = self.api_url
842846
while nexturl:
843-
try:
844-
resp: Response = session.get(nexturl)
845-
resp.raise_for_status()
846-
except Exception as e:
847-
logger.error(
848-
"%s: Failed to fetch GitLab tags from %s: %s",
849-
identifier,
850-
nexturl,
851-
e,
852-
)
853-
raise
847+
resp: Response = session.get(nexturl)
848+
resp.raise_for_status()
854849
for entry in resp.json():
855850
name = entry["name"]
856851
version = self._match_function(identifier, name)

0 commit comments

Comments
 (0)