-
Notifications
You must be signed in to change notification settings - Fork 0
fix runtime heartbeat API retries #133
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,11 +7,18 @@ | |
| import json | ||
| import os | ||
| import sys | ||
| import time | ||
| import urllib.error | ||
| import urllib.parse | ||
| import urllib.request | ||
| from typing import Any | ||
|
|
||
|
|
||
| _GITHUB_API_MAX_ATTEMPTS = 4 | ||
| _GITHUB_API_MAX_RETRY_DELAY_SECONDS = 30.0 | ||
| _RETRYABLE_HTTP_STATUSES = frozenset({429, 500, 502, 503, 504}) | ||
|
|
||
|
|
||
| def _split_values(raw: str | None) -> list[str]: | ||
| if not raw: | ||
| return [] | ||
|
|
@@ -37,6 +44,16 @@ def _parse_timestamp(value: Any) -> dt.datetime | None: | |
| return parsed.astimezone(dt.timezone.utc) | ||
|
|
||
|
|
||
| def _github_retry_delay(exc: urllib.error.HTTPError | urllib.error.URLError, attempt: int) -> float: | ||
| retry_after = exc.headers.get("Retry-After") if isinstance(exc, urllib.error.HTTPError) else None | ||
| if retry_after: | ||
| try: | ||
| return min(max(float(retry_after), 0.0), _GITHUB_API_MAX_RETRY_DELAY_SECONDS) | ||
| except ValueError: | ||
| pass | ||
| return min(float(2 ** (attempt - 1)), _GITHUB_API_MAX_RETRY_DELAY_SECONDS) | ||
|
|
||
|
|
||
| def _github_request(url: str, token: str) -> dict[str, Any]: | ||
| request = urllib.request.Request( | ||
| url, | ||
|
|
@@ -46,8 +63,32 @@ def _github_request(url: str, token: str) -> dict[str, Any]: | |
| "X-GitHub-Api-Version": "2022-11-28", | ||
| }, | ||
| ) | ||
| with urllib.request.urlopen(request, timeout=20) as response: | ||
| return json.loads(response.read().decode("utf-8")) | ||
| for attempt in range(1, _GITHUB_API_MAX_ATTEMPTS + 1): | ||
| retry_error: urllib.error.HTTPError | urllib.error.URLError | None = None | ||
| try: | ||
| with urllib.request.urlopen(request, timeout=20) as response: | ||
| return json.loads(response.read().decode("utf-8")) | ||
| except urllib.error.HTTPError as exc: | ||
| retryable = exc.code in _RETRYABLE_HTTP_STATUSES | ||
| if not retryable or attempt == _GITHUB_API_MAX_ATTEMPTS: | ||
| raise | ||
| retry_error = exc | ||
| except urllib.error.URLError as exc: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the GitHub endpoint stalls past the explicit 20-second timeout, CPython's Useful? React with 👍 / 👎. |
||
| if attempt == _GITHUB_API_MAX_ATTEMPTS: | ||
| raise | ||
| retry_error = exc | ||
|
|
||
| assert retry_error is not None | ||
| delay = _github_retry_delay(retry_error, attempt) | ||
| print( | ||
| "GitHub API request failed " | ||
| f"({retry_error}); retrying in {delay:g}s " | ||
| f"(attempt {attempt + 1}/{_GITHUB_API_MAX_ATTEMPTS})", | ||
| file=sys.stderr, | ||
| ) | ||
| time.sleep(delay) | ||
|
|
||
| raise RuntimeError("unreachable") | ||
|
|
||
|
|
||
| def _workflow_paths(workflow: str) -> set[str]: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GitHub can report primary or secondary rate-limit exhaustion with HTTP 403 (distinguishable from authorization failures by rate-limit/
Retry-Afterheaders), but this status is treated as permanently non-retryable. In that rate-limit case the heartbeat fails without an alert message instead of waiting and retrying, even though the API request will become valid again.Useful? React with 👍 / 👎.