Skip to content

Commit 7b19e7c

Browse files
committed
add a retry_on_error decoration for gardenlinux.oci
1 parent 49d0a52 commit 7b19e7c

File tree

2 files changed

+104
-0
lines changed

2 files changed

+104
-0
lines changed

src/gardenlinux/oci/helper.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import functools
2+
import logging
3+
import sys
4+
import time
5+
6+
from requests.exceptions import RequestException, HTTPError
7+
8+
logger = logging.getLogger(__name__)
9+
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
10+
11+
12+
def retry_on_error(
13+
max_retries=3,
14+
initial_delay=1,
15+
backoff_factor=2,
16+
# retry on:
17+
# - 502 - bad gateway
18+
# - 503 - service unavailable
19+
# - 504 - gateway timeout
20+
# - 429 - too many requests
21+
# - 400 - bad request (e.g. invalid range start for blob upload)
22+
# - 404 - not found (e.g. blob not found is seen in unit tests)
23+
retryable_status_codes=(502, 503, 504, 429, 400, 404),
24+
retryable_exceptions=(RequestException,),
25+
):
26+
"""
27+
A decorator for retrying functions that might fail due to transient network issues.
28+
29+
Args:
30+
max_retries: Maximum number of retry attempts
31+
initial_delay: Initial delay between retries in seconds
32+
backoff_factor: Factor by which the delay increases with each retry
33+
retryable_status_codes: HTTP status codes that trigger a retry
34+
retryable_exceptions: Exception types that trigger a retry
35+
36+
Returns:
37+
Decorated function with retry logic
38+
"""
39+
40+
def decorator(func):
41+
@functools.wraps(func)
42+
def wrapper(*args, **kwargs):
43+
delay = initial_delay
44+
last_exception = None
45+
46+
for retry_count in range(max_retries + 1):
47+
try:
48+
if retry_count > 0:
49+
logger.info(
50+
f"Retry attempt {retry_count}/{max_retries} for {func.__name__}"
51+
)
52+
53+
response = func(*args, **kwargs)
54+
55+
# Check for retryable status codes in the response
56+
if (
57+
hasattr(response, "status_code")
58+
and response.status_code in retryable_status_codes
59+
):
60+
status_code = response.status_code
61+
logger.warning(
62+
f"Received status code {status_code} from {func.__name__}, retrying..."
63+
)
64+
last_exception = HTTPError(f"HTTP Error {status_code}")
65+
else:
66+
# Success, return the response
67+
return response
68+
69+
except retryable_exceptions as e:
70+
logger.warning(f"Request failed in {func.__name__}: {str(e)}")
71+
last_exception = e
72+
73+
# Don't sleep if this was the last attempt
74+
if retry_count < max_retries:
75+
sleep_time = delay * (backoff_factor**retry_count)
76+
logger.info(f"Waiting {sleep_time:.2f} seconds before retry")
77+
time.sleep(sleep_time)
78+
79+
# If we got here, all retries failed
80+
logger.error(f"All {max_retries} retries failed for {func.__name__}")
81+
if last_exception:
82+
raise last_exception
83+
return None
84+
85+
return wrapper
86+
87+
return decorator

src/gardenlinux/oci/registry.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
calculate_sha256,
3535
verify_sha256,
3636
)
37+
from .helper import retry_on_error
3738
from python_gardenlinux_lib.features.parse_features import get_oci_metadata_from_fileset
3839
from .schemas import (
3940
EmptyIndex,
@@ -646,6 +647,22 @@ def create_layer(
646647
}
647648
return layer
648649

650+
@retry_on_error(max_retries=3, initial_delay=2, backoff_factor=2)
651+
def upload_blob(self, file_path, container, metadata=None):
652+
"""
653+
Upload a blob to the registry with retry logic for network errors.
654+
655+
Args:
656+
file_path: Path to the file to upload
657+
container: Container object
658+
metadata: Optional metadata for the blob
659+
660+
Returns:
661+
Response from the upload
662+
"""
663+
# Call the parent class's upload_blob method
664+
return super().upload_blob(file_path, container, metadata)
665+
649666
def push_from_dir(
650667
self,
651668
architecture: str,

0 commit comments

Comments
 (0)