1313# See the License for the specific language governing permissions and
1414# limitations under the License.
1515#
16+ import logging
17+ import asyncio
1618
1719from aiohttp import ClientSession , ClientResponse , ClientResponseError
20+ from typing import (
21+ AnyStr ,
22+ Callable ,
23+ Optional ,
24+ Dict ,
25+ TypeAlias ,
26+ Union ,
27+ TextIO ,
28+ Awaitable ,
29+ Any ,
30+ )
1831from pathlib import Path
19- from typing import AnyStr , Callable , Optional , Dict , TypeAlias , Union , TextIO
2032from dremioai .log import logger
2133from json import loads
2234from pydantic import BaseModel , ValidationError
35+ from http import HTTPStatus
2336
2437from dremioai .config import settings
2538from dremioai .api .oauth2 import get_oauth2_tokens
2639
2740DeserializationStrategy : TypeAlias = Union [Callable , BaseModel ]
2841
2942
43+ class RetryConfig :
44+ def __init__ (self ):
45+ if settings .instance () and settings .instance ().dremio :
46+ self .config = settings .instance ().dremio .http_retry
47+ else :
48+ self .config = settings .HttpRetry ()
49+
50+ @property
51+ def max_retries (self ) -> int :
52+ """Expose max_retries from config for convenience"""
53+ return self .config .max_retries
54+
55+ def get_config_delay (self , attempt_number : int = 0 ) -> float :
56+ return self .config .initial_delay * (
57+ self .config .backoff_multiplier ** attempt_number
58+ )
59+
60+ def get_delay (
61+ self ,
62+ response : ClientResponse ,
63+ attempt_number : int ,
64+ ) -> float :
65+ retry_after = response .headers .get ("Retry-After" )
66+ delay = self .get_config_delay (attempt_number = attempt_number )
67+ if retry_after is not None :
68+ try :
69+ delay = min (delay , int (retry_after ))
70+ except (ValueError , TypeError ) as e :
71+ logger ().debug (
72+ f"Invalid Retry-After header, using exponential backoff - { e } "
73+ )
74+
75+ return min (delay , self .config .max_delay )
76+
77+
78+ async def retry_middleware (
79+ req , handler : Callable [[any ], Awaitable [ClientResponse ]]
80+ ) -> ClientResponse :
81+ """
82+ Middleware that automatically retries requests on 429 (rate limit) errors.
83+ Uses exponential backoff with configurable parameters from settings.
84+ """
85+ retry_config = RetryConfig ()
86+ for attempt in range (retry_config .max_retries + 1 ):
87+ response = await handler (req )
88+ if response .status != HTTPStatus .TOO_MANY_REQUESTS :
89+ break
90+
91+ delay = retry_config .get_delay (response , attempt )
92+ logger (f"{ __name__ } .retry" ).warning (
93+ f"Rate limited (429) on { req .method } { req .url .path } . "
94+ f"Retry { attempt + 1 } /{ retry_config .max_retries } after { delay :.2f} s"
95+ )
96+ await asyncio .sleep (delay )
97+
98+ return response
99+
100+
30101class AsyncHttpClient :
31102 def __init__ (self , uri : AnyStr , token : AnyStr ):
32103 self .uri = uri
@@ -83,6 +154,18 @@ async def handle_response(
83154 )
84155 await self .download (response , file )
85156
157+ def log_request (
158+ self , method : str , endpoint : str , params : Optional [Dict [AnyStr , Any ]] = None
159+ ):
160+ if logger ().isEnabledFor (logging .DEBUG ):
161+ sanitized_headers = {
162+ k : (v if k != "Authorization" else "Bearer <redacted>" )
163+ for k , v in self .headers .items ()
164+ }
165+ logger ().debug (
166+ f"{ method } { self .uri } { endpoint } ', headers={ sanitized_headers } , params={ params } "
167+ )
168+
86169 async def get (
87170 self ,
88171 endpoint : AnyStr ,
@@ -92,10 +175,8 @@ async def get(
92175 file : Optional [TextIO ] = None ,
93176 top_level_list : bool = False ,
94177 ):
95- async with ClientSession () as session :
96- logger ().info (
97- f"{ self .uri } { endpoint } ', headers={ self .headers } , params={ params } "
98- )
178+ async with ClientSession (middlewares = (retry_middleware ,)) as session :
179+ self .log_request ("GET" , endpoint , params )
99180 async with session .get (
100181 f"{ self .uri } { endpoint } " ,
101182 headers = self .headers ,
@@ -115,7 +196,8 @@ async def post(
115196 file : Optional [TextIO ] = None ,
116197 top_level_list : bool = False ,
117198 ):
118- async with ClientSession () as session :
199+ async with ClientSession (middlewares = (retry_middleware ,)) as session :
200+ self .log_request ("POST" , endpoint )
119201 async with session .post (
120202 f"{ self .uri } { endpoint } " , headers = self .headers , json = body , ssl = False
121203 ) as response :
0 commit comments