From 6da1025adc016b82a95d1820bae1e97484ab5a47 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 13 May 2026 19:41:15 +0000 Subject: [PATCH] Retry transient GitHub API fetch failures Co-authored-by: Armen Zambrano G. --- src/github_sdk.py | 26 ++++++++++++++++++++++--- tests/test_github_sdk.py | 42 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/src/github_sdk.py b/src/github_sdk.py index cf28d01..0b9f792 100644 --- a/src/github_sdk.py +++ b/src/github_sdk.py @@ -4,6 +4,7 @@ import hashlib import io import logging +import time import uuid from datetime import datetime @@ -11,6 +12,15 @@ from sentry_sdk.envelope import Envelope from sentry_sdk.utils import format_timestamp +GITHUB_REQUEST_RETRIES = 3 +GITHUB_REQUEST_TIMEOUT = 10 +GITHUB_RETRY_BACKOFF_SECONDS = 0.25 +GITHUB_TRANSIENT_ERRORS = ( + requests.exceptions.ConnectionError, + requests.exceptions.SSLError, + requests.exceptions.Timeout, +) + class GithubSentryError(Exception): pass @@ -42,9 +52,19 @@ def __init__(self, token, dsn, dry_run=False) -> None: def _fetch_github(self, url): headers = {"Authorization": f"token {self.token}"} - req = requests.get(url, headers=headers) - req.raise_for_status() - return req + for attempt in range(GITHUB_REQUEST_RETRIES): + try: + req = requests.get( + url, + headers=headers, + timeout=GITHUB_REQUEST_TIMEOUT, + ) + req.raise_for_status() + return req + except GITHUB_TRANSIENT_ERRORS: + if attempt == GITHUB_REQUEST_RETRIES - 1: + raise + time.sleep(GITHUB_RETRY_BACKOFF_SECONDS * 2**attempt) def _get_extra_metadata(self, job): # XXX: This is the slowest call diff --git a/tests/test_github_sdk.py b/tests/test_github_sdk.py index 7f7e401..51211e5 100644 --- a/tests/test_github_sdk.py +++ b/tests/test_github_sdk.py @@ -2,7 +2,7 @@ import sys from datetime import datetime -from unittest.mock import patch +from unittest.mock import Mock, patch import pytest import requests @@ -11,7 +11,7 @@ from requests import HTTPError from sentry_sdk.utils import format_timestamp -from src.github_sdk import GithubClient +from src.github_sdk import GithubClient, GITHUB_REQUEST_TIMEOUT DSN = "https://foo@random.ingest.sentry.io/bar" TOKEN = "irrelevant" @@ -59,6 +59,44 @@ def test_ensure_raise_error_on_github_api_failure(): ) +@patch("src.github_sdk.time.sleep") +@patch("src.github_sdk.requests.get") +def test_retries_transient_github_api_failure(mock_get, mock_sleep): + url = "https://api.github.com/repos/getsentry/sentry/actions/runs/123" + response = Mock() + response.raise_for_status.return_value = None + mock_get.side_effect = [ + requests.exceptions.SSLError("transient TLS failure"), + response, + ] + + client = GithubClient(dsn=DSN, token=TOKEN) + + assert client._fetch_github(url) is response + assert mock_get.call_count == 2 + mock_get.assert_called_with( + url, + headers={"Authorization": f"token {TOKEN}"}, + timeout=GITHUB_REQUEST_TIMEOUT, + ) + mock_sleep.assert_called_once_with(0.25) + + +@patch("src.github_sdk.time.sleep") +@patch("src.github_sdk.requests.get") +def test_exhausts_transient_github_api_retries(mock_get, mock_sleep): + url = "https://api.github.com/repos/getsentry/sentry/actions/runs/123" + mock_get.side_effect = requests.exceptions.SSLError("transient TLS failure") + + client = GithubClient(dsn=DSN, token=TOKEN) + + with pytest.raises(requests.exceptions.SSLError): + client._fetch_github(url) + + assert mock_get.call_count == 3 + assert mock_sleep.call_count == 2 + + @freeze_time() @responses.activate @patch("src.github_sdk.get_uuid")