From f77841e820b944f221a4c61e7357f5cc52a62beb Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sun, 13 Apr 2025 22:16:08 +0400 Subject: [PATCH 1/2] feat: retry network errors --- infisical_sdk/infisical_requests.py | 61 ++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/infisical_sdk/infisical_requests.py b/infisical_sdk/infisical_requests.py index 89ef9f4..0f3778c 100644 --- a/infisical_sdk/infisical_requests.py +++ b/infisical_sdk/infisical_requests.py @@ -1,9 +1,26 @@ -from typing import Any, Dict, Generic, Optional, TypeVar, Type +from typing import Any, Dict, Generic, Optional, TypeVar, Type, Callable, List +import socket import requests +import functools from dataclasses import dataclass +import time T = TypeVar("T") +# List of network-related exceptions that should trigger retries +NETWORK_ERRORS = [ + requests.exceptions.ConnectionError, + requests.exceptions.ChunkedEncodingError, + requests.exceptions.ReadTimeout, + requests.exceptions.ConnectTimeout, + socket.gaierror, + socket.timeout, + ConnectionResetError, + ConnectionRefusedError, + ConnectionError, + ConnectionAbortedError, +] + def join_url(base: str, path: str) -> str: """ Join base URL and path properly, handling slashes appropriately. @@ -49,6 +66,44 @@ def from_dict(cls, data: Dict) -> 'APIResponse[T]': headers=data['headers'] ) +def with_retry( + max_retries: int = 3, + base_delay: float = 1.0, + network_errors: Optional[List[Type[Exception]]] = None +) -> Callable: + """ + Decorator to add retry logic with exponential backoff to requests methods. + """ + if network_errors is None: + network_errors = NETWORK_ERRORS + + def decorator(func: Callable) -> Callable: + @functools.wraps(func) + def wrapper(*args, **kwargs): + retry_count = 0 + + while True: + try: + return func(*args, **kwargs) + except tuple(network_errors) as error: + retry_count += 1 + if retry_count > max_retries: + print(f"Max retries ({max_retries}) exceeded. Giving up.") + raise + + delay = base_delay * (2 ** (retry_count - 1)) + + print( + f"Network error {error.__class__.__name__}: {str(error)}. " + f"Retrying in {delay:.2f}s (attempt {retry_count}/{max_retries})" + ) + + time.sleep(delay) + + return wrapper + + return decorator + class InfisicalRequests: def __init__(self, host: str, token: Optional[str] = None): @@ -93,6 +148,7 @@ def _handle_response(self, response: requests.Response) -> Dict[str, Any]: except ValueError: raise InfisicalError("Invalid JSON response") + @with_retry(max_retries=4, base_delay=1.0) def get( self, path: str, @@ -119,6 +175,7 @@ def get( headers=dict(response.headers) ) + @with_retry(max_retries=4, base_delay=1.0) def post( self, path: str, @@ -143,6 +200,7 @@ def post( headers=dict(response.headers) ) + @with_retry(max_retries=4, base_delay=1.0) def patch( self, path: str, @@ -167,6 +225,7 @@ def patch( headers=dict(response.headers) ) + @with_retry(max_retries=4, base_delay=1.0) def delete( self, path: str, From b276b4121a0040679f81f295389107357d67c70f Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 17 Apr 2025 00:00:07 +0400 Subject: [PATCH 2/2] requested changes --- infisical_sdk/infisical_requests.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/infisical_sdk/infisical_requests.py b/infisical_sdk/infisical_requests.py index 0f3778c..0b91516 100644 --- a/infisical_sdk/infisical_requests.py +++ b/infisical_sdk/infisical_requests.py @@ -4,6 +4,7 @@ import functools from dataclasses import dataclass import time +import random T = TypeVar("T") @@ -88,15 +89,13 @@ def wrapper(*args, **kwargs): except tuple(network_errors) as error: retry_count += 1 if retry_count > max_retries: - print(f"Max retries ({max_retries}) exceeded. Giving up.") raise - delay = base_delay * (2 ** (retry_count - 1)) + base_delay_with_backoff = base_delay * (2 ** (retry_count - 1)) - print( - f"Network error {error.__class__.__name__}: {str(error)}. " - f"Retrying in {delay:.2f}s (attempt {retry_count}/{max_retries})" - ) + # +/-20% jitter + jitter = random.uniform(-0.2, 0.2) * base_delay_with_backoff + delay = base_delay_with_backoff + jitter time.sleep(delay)