@@ -73,6 +73,56 @@ def setup_logging(level=None):
7373if ClientPayloadError is not None :
7474 S3_RETRYABLE_ERRORS += (ClientPayloadError ,)
7575
76+
77+ def add_retryable_error (exc ):
78+ """
79+ Add an exception type to the list of retryable S3 errors.
80+
81+ Parameters
82+ ----------
83+ exc : Exception
84+ The exception type to add to the retryable errors.
85+
86+ Examples
87+ ----------
88+ >>> class MyCustomError(Exception): # doctest: +SKIP
89+ ... pass # doctest: +SKIP
90+ >>> add_retryable_error(MyCustomError) # doctest: +SKIP
91+ """
92+ global S3_RETRYABLE_ERRORS
93+ S3_RETRYABLE_ERRORS += (exc ,)
94+
95+
96+ CUSTOM_ERROR_HANDLER = lambda _ : False
97+
98+
99+ def set_custom_error_handler (func ):
100+ """Set a custom error handler function for S3 retryable errors.
101+
102+ The function should take an exception instance as its only argument,
103+ and return True if the operation should be retried, or False otherwise.
104+ This can also be used for custom behavior on `ClientError` exceptions,
105+ such as retrying other patterns.
106+
107+ Parameters
108+ ----------
109+ func : callable[[Exception], bool]
110+ The custom error handler function.
111+
112+ Examples
113+ ----------
114+ >>> def my_handler(e): # doctest: +SKIP
115+ ... return isinstance(e, MyCustomError) and "some condition" in str(e) # doctest: +SKIP
116+ >>> set_custom_error_handler(my_handler) # doctest: +SKIP
117+
118+ >>> def another_handler(e): # doctest: +SKIP
119+ ... return isinstance(e, ClientError) and "Throttling" in str(e)" # doctest: +SKIP
120+ >>> set_custom_error_handler(another_handler) # doctest: +SKIP
121+ """
122+ global CUSTOM_ERROR_HANDLER
123+ CUSTOM_ERROR_HANDLER = func
124+
125+
76126_VALID_FILE_MODES = {"r" , "w" , "a" , "rb" , "wb" , "ab" }
77127
78128_PRESERVE_KWARGS = [
@@ -110,29 +160,46 @@ def setup_logging(level=None):
110160async def _error_wrapper (func , * , args = (), kwargs = None , retries ):
111161 if kwargs is None :
112162 kwargs = {}
163+ err = None
113164 for i in range (retries ):
165+ wait_time = min (1.7 ** i * 0.1 , 15 )
166+
114167 try :
115168 return await func (* args , ** kwargs )
116169 except S3_RETRYABLE_ERRORS as e :
117170 err = e
118171 logger .debug ("Retryable error: %s" , e )
119- await asyncio .sleep (min ( 1.7 ** i * 0.1 , 15 ) )
172+ await asyncio .sleep (wait_time )
120173 except ClientError as e :
121174 logger .debug ("Client error (maybe retryable): %s" , e )
122175 err = e
123- wait_time = min (1.7 ** i * 0.1 , 15 )
124- if "SlowDown" in str (e ):
125- await asyncio .sleep (wait_time )
126- elif "reduce your request rate" in str (e ):
127- await asyncio .sleep (wait_time )
128- elif "XAmzContentSHA256Mismatch" in str (e ):
176+
177+ matched = False
178+ for pattern in [
179+ "SlowDown" ,
180+ "reduce your request rate" ,
181+ "XAmzContentSHA256Mismatch" ,
182+ ]:
183+ if pattern in str (e ):
184+ matched = True
185+ break
186+
187+ if matched :
129188 await asyncio .sleep (wait_time )
130189 else :
131- break
190+ should_retry = CUSTOM_ERROR_HANDLER (e )
191+ if should_retry :
192+ await asyncio .sleep (wait_time )
193+ else :
194+ break
132195 except Exception as e :
133- logger .debug ("Nonretryable error: %s" , e )
134196 err = e
135- break
197+ should_retry = CUSTOM_ERROR_HANDLER (e )
198+ if should_retry :
199+ await asyncio .sleep (wait_time )
200+ else :
201+ logger .debug ("Nonretryable error: %s" , e )
202+ break
136203
137204 if "'coroutine'" in str (err ):
138205 # aiobotocore internal error - fetch original botocore error
0 commit comments