Skip to content

Commit 3bd93fd

Browse files
authored
Allow custom error handling (#1003)
1 parent 5e5f7ea commit 3bd93fd

File tree

4 files changed

+394
-11
lines changed

4 files changed

+394
-11
lines changed

docs/source/index.rst

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,67 @@ Python's standard `logging framework`_.
154154

155155
.. _logging framework: https://docs.python.org/3/library/logging.html
156156

157+
Errors
158+
------
159+
160+
The ``s3fs`` library includes a built-in mechanism to automatically retry
161+
operations when specific transient errors occur. You can customize this behavior
162+
by adding specific exception types or defining complex logic via custom handlers.
163+
164+
Default Retryable Errors
165+
~~~~~~~~~~~~~~~~~~~~~~~~
166+
167+
By default, ``s3fs`` will retry the following exception types:
168+
169+
- ``socket.timeout``
170+
- ``HTTPClientError``
171+
- ``IncompleteRead``
172+
- ``FSTimeoutError``
173+
- ``ResponseParserError``
174+
- ``aiohttp.ClientPayloadError`` (if available)
175+
176+
Registering Custom Error Types
177+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
178+
179+
To include additional exception types in the default retry logic, use the
180+
``add_retryable_error`` function. This is useful for simple type-based retries.
181+
182+
.. code-block:: python
183+
184+
>>> class MyCustomError(Exception):
185+
pass
186+
>>> s3fs.add_retryable_error(MyCustomError)
187+
188+
Implementing Custom Error Handlers
189+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
190+
191+
For more complex scenarios, such as retrying based on an error message rather than
192+
just the type, you can register a custom error handler using ``set_custom_error_handler``.
193+
194+
The handler should be a callable that accepts an exception instance and returns ``True``
195+
if the error should be retried, or ``False`` otherwise.
196+
197+
.. code-block:: python
198+
199+
>>> def my_handler(e):
200+
return isinstance(e, MyCustomError) and "some condition" in str(e)
201+
>>> s3fs.set_custom_error_handler(my_handler)
202+
203+
Handling AWS ClientErrors
204+
~~~~~~~~~~~~~~~~~~~~~~~~~
205+
206+
``s3fs`` provides specialized handling for ``botocore.exceptions.ClientError``.
207+
While ``s3fs`` checks these against internal patterns (like throttling),
208+
you can extend this behavior using a custom handler. Note that the internal
209+
patterns will still be checked and handled before the custom handler.
210+
211+
.. code-block:: python
212+
213+
>>> def another_handler(e):
214+
return isinstance(e, ClientError) and "Throttling" in str(e)
215+
>>> s3fs.set_custom_error_handler(another_handler)
216+
217+
157218
Credentials
158219
-----------
159220

s3fs/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .core import S3FileSystem, S3File
1+
from .core import S3FileSystem, S3File, add_retryable_error, set_custom_error_handler
22
from .mapping import S3Map
33

44
from ._version import get_versions

s3fs/core.py

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,56 @@ def setup_logging(level=None):
7373
if 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):
110160
async 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

Comments
 (0)