Skip to content

Commit

Permalink
feat(whitelist): add whitelist and blacklist support
Browse files Browse the repository at this point in the history
  • Loading branch information
h2non committed Dec 30, 2016
1 parent 8f33f60 commit 39cad3b
Show file tree
Hide file tree
Showing 14 changed files with 255 additions and 33 deletions.
14 changes: 11 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ riprova |Build Status| |PyPI| |Coverage Status| |Documentation Status| |Quality|
``riprova`` (meaning ``retry`` in Italian) is a small, general-purpose and versatile `Python`_ library
providing retry mechanisms with multiple backoff strategies for failed operations.

It's domain agnostic and can be used within any code base.

For a brief introduction about backoff mechanisms for potential failed operations, `read this article`_.


Expand All @@ -13,9 +15,10 @@ Features
- Retry decorator for simple and idiomatic consumption.
- Simple Pythonic programmatic interface.
- Maximum retry timeout support.
- Supports error whitelisting and custom error evaluator logic.
- Supports error `whitelisting`_ and `blacklisting`_.
- Supports custom `error evaluation`_ retry logic (useful to retry only in specific cases).
- Automatically retry operations on raised exceptions.
- Supports asynchronous coroutines with both ``async/await`` and ``yield from`` syntax.
- Supports `asynchronous coroutines`_ with both ``async/await`` and ``yield from`` syntax.
- Configurable maximum number of retry attempts.
- Custom retry evaluator function, useful to determine when an operation failed or not.
- Highly configurable supporting max retries, timeouts or retry notifier callback.
Expand Down Expand Up @@ -65,6 +68,7 @@ API
- riprova.FibonacciBackoff_
- riprova.ExponentialBackoff_
- riprova.ErrorWhitelist_
- riprova.ErrorBlacklist_
- riprova.add_whitelist_error_
- riprova.RetryError_
- riprova.RetryTimeoutError_
Expand All @@ -80,6 +84,7 @@ API
.. _riprova.FibonacciBackoff: http://riprova.readthedocs.io/en/latest/api.html#riprova.FibonacciBackoff
.. _riprova.ExponentialBackoff: http://riprova.readthedocs.io/en/latest/api.html#riprova.ExponentialBackoff
.. _riprova.ErrorWhitelist: http://riprova.readthedocs.io/en/latest/api.html#riprova.ErrorWhitelist
.. _riprova.ErrorBlacklist: http://riprova.readthedocs.io/en/latest/api.html#riprova.ErrorBlacklist
.. _riprova.add_whitelist_error: http://riprova.readthedocs.io/en/latest/api.html#riprova.add_whitelist_error
.. _riprova.RetryError: http://riprova.readthedocs.io/en/latest/api.html#riprova.RetryError
.. _riprova.RetryTimeoutError: http://riprova.readthedocs.io/en/latest/api.html#riprova.RetryTimeoutError
Expand Down Expand Up @@ -193,7 +198,10 @@ MIT - Tomas Aparicio
.. _Fibonacci backoff: http://riprova.readthedocs.io/en/latest/api.html#riprova.FibonacciBackoff
.. _Exponential backoff: http://riprova.readthedocs.io/en/latest/api.html#riprova.ExponentialBackOff
.. _ConstantBackoff: https://github.com/h2non/riprova/blob/master/riprova/strategies/constant.py

.. _whitelisting: https://github.com/h2non/riprova/blob/master/examples/whitelisting_errors.py
.. _blacklisting: https://github.com/h2non/riprova/blob/master/examples/blacklisting_errors.py
.. _error evaluation: https://github.com/h2non/riprova/blob/master/examples/http_request.py
.. _asynchronous coroutines: https://github.com/h2non/riprova/blob/master/examples/http_asyncio.py

.. |Build Status| image:: https://travis-ci.org/h2non/riprova.svg?branch=master
:target: https://travis-ci.org/h2non/riprova
Expand Down
12 changes: 12 additions & 0 deletions docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ Retry failed HTTP requests using asyncio + aiohttp
.. literalinclude:: ../examples/http_asyncio.py


Whitelisting custom errors
^^^^^^^^^^^^^^^^^^^^^^^^^^

.. literalinclude:: ../examples/whitelisting_errors.py


Blacklisting custom errors
^^^^^^^^^^^^^^^^^^^^^^^^^^

.. literalinclude:: ../examples/blacklisting_errors.py


Constant backoff strategy
^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
56 changes: 56 additions & 0 deletions examples/blacklisting_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
import riprova


# Custom error object
class MyCustomError(Exception):
pass


# Blacklist of errors that should exclusively be retried
blacklist = riprova.ErrorBlacklist([
RuntimeError,
MyCustomError
])


def error_evaluator(error):
"""
Used to determine if an error is legit and therefore
should be retried or not.
"""
return blacklist.isretry(error)


# In order to define a global blacklist policy that would be used
# across all retry instances, overwrite the blacklist attribute in Retrier.
# NOTE: blacklist overwrites any whitelist. They are mutually exclusive.
riprova.Retrier.blacklist = blacklist

# Store number of function calls for error simulation
calls = 0


# Register retriable operation with a custom error evaluator
# You should pass the evaluator per retry instance.
@riprova.retry(error_evaluator=error_evaluator)
def mul2(x):
global calls

if calls < 3:
calls += 1
raise RuntimeError('simulated call error')

if calls == 3:
calls += 1
raise Exception('non blacklisted error')

return x * 2


# Run task
try:
mul2(2)
except Exception as err:
print('Blacklisted error: {}'.format(err))
print('Retry attempts: {}'.format(calls))
2 changes: 1 addition & 1 deletion examples/constant_backoff.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ def mul2(x):

# Run task
result = mul2(2)
print('Result:', result)
print('Result: {}'.format(result))
2 changes: 1 addition & 1 deletion examples/decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ def mul2(x):

# Run task
result = mul2(2)
print('Result:', result)
print('Result: {}'.format(result))
2 changes: 1 addition & 1 deletion examples/exponential_backoff.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ def mul2(x):

# Run task
result = mul2(2)
print('Result:', result)
print('Result: {}'.format(result))
2 changes: 1 addition & 1 deletion examples/fibonacci_backoff.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ def mul2(x):

# Run task
result = mul2(2)
print('Result:', result)
print('Result: {}'.format(result))
2 changes: 1 addition & 1 deletion examples/timeout_limit.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ def mul2(x):
try:
mul2(2)
except riprova.RetryTimeoutError as err:
print('Timeout error:', err)
print('Timeout error: {}'.format(err))
58 changes: 58 additions & 0 deletions examples/whitelisting_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
import riprova


# Custom error object
class MyCustomError(Exception):
pass


# Whitelist of errors that should not be retried
whitelist = riprova.ErrorWhitelist([
ReferenceError,
ImportError,
IOError,
SyntaxError,
IndexError
])


def error_evaluator(error):
"""
Used to determine if an error is legit and therefore
should be retried or not.
"""
return whitelist.isretry(error)


# In order to define a global whitelist policy that would be used
# across all retry instances, overwrite the whitelist attribute in Retrier:
riprova.Retrier.whitelist = whitelist

# Store number of function calls for error simulation
calls = 0


# Register retriable operation with a custom error evaluator
# You should pass the evaluator per retry instance.
@riprova.retry(error_evaluator=error_evaluator)
def mul2(x):
global calls

if calls < 3:
calls += 1
raise RuntimeError('simulated call error')

if calls == 3:
calls += 1
raise ReferenceError('legit error')

return x * 2


# Run task
try:
mul2(2)
except ReferenceError as err:
print('Whitelisted error: {}'.format(err))
print('Retry attempts: {}'.format(calls))
3 changes: 2 additions & 1 deletion riprova/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from .retrier import Retrier
from .backoff import Backoff
from .constants import PY_34
from .whitelist import ErrorWhitelist, add_whitelist_error
from .errors import ErrorWhitelist, ErrorBlacklist, add_whitelist_error
from .strategies import * # noqa
from .exceptions import (RetryError, MaxRetriesExceeded,
RetryTimeoutError, NotRetriableError)
Expand All @@ -24,6 +24,7 @@
'FibonacciBackoff',
'ExponentialBackOff',
'ErrorWhitelist',
'ErrorBlacklist',
'add_whitelist_error',
'RetryError',
'MaxRetriesExceeded',
Expand Down
16 changes: 13 additions & 3 deletions riprova/async_retrier.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from .backoff import Backoff
from .retrier import Retrier
from .constants import PY_34
from .whitelist import ErrorWhitelist
from .errors import ErrorWhitelist
from .strategies import ConstantBackoff
from .exceptions import MaxRetriesExceeded, RetryError

Expand Down Expand Up @@ -69,6 +69,9 @@ class AsyncRetrier(Retrier):
Attributes:
whitelist (riprova.ErrorWhitelist): default error whitelist instance
used to evaluate when.
blacklist (riprova.ErrorBlacklist): default error blacklist instance
used to evaluate when.
Blacklist and Whitelist are mutually exclusive.
timeout (int): stores the maximum retries attempts timeout in
milliseconds. Defaults to `0`.
attempts (int): number of retry attempts being executed from last
Expand Down Expand Up @@ -106,8 +109,11 @@ async def task(x):
assert retrier.error == None
"""

# Stores the default error whitelist used for error retry evaluation
whitelist = ErrorWhitelist()
# Stores the default global error whitelist used for error retry evaluation
whitelist = None

# Blacklist is just a semantic alias to whitelist
blacklist = None

def __init__(self,
timeout=0,
Expand Down Expand Up @@ -139,6 +145,10 @@ def __init__(self,
self.backoff = backoff or ConstantBackoff()
# Function used to sleep. Defaults `asyncio.sleep()`.
self.sleep = sleep_coro or asyncio.sleep
# Stores the default error whitelist used for error retry evaluation
self.whitelist = (AsyncRetrier.blacklist or
AsyncRetrier.whitelist or
ErrorWhitelist())

@asyncio.coroutine
def _call(self, coro, *args, **kw):
Expand Down
46 changes: 36 additions & 10 deletions riprova/whitelist.py → riprova/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class ErrorWhitelist(object):
# engine. User can mutate and extend this list via class method.
WHITELIST = set([
SystemExit,
IndexError,
ImportError,
SyntaxError,
ReferenceError,
Expand All @@ -27,8 +28,7 @@ class ErrorWhitelist(object):
])

def __init__(self, errors=None):
self._whitelist = set(errors if errors else
ErrorWhitelist.WHITELIST.copy())
self._list = set(errors if errors else ErrorWhitelist.WHITELIST.copy())

@property
def errors(self):
Expand All @@ -39,7 +39,7 @@ def errors(self):
errors (list|tuple[Exception]): iterable containing errors to
whitelist.
"""
return self._whitelist
return self._list

@errors.setter
def errors(self, errors):
Expand All @@ -52,11 +52,11 @@ def errors(self, errors):
if not isinstance(errors, (list, tuple)):
raise TypeError('errors must be a list or tuple')

self._whitelist = set()
self._list = set()
for err in errors:
if not issubclass(err, BaseException):
raise TypeError('error must be a subclass of Exception')
self._whitelist.add(err)
self._list.add(err)

def add(self, *errors):
"""
Expand All @@ -66,25 +66,51 @@ def add(self, *errors):
*errors (Exception): variadic error classes to add.
"""
# Cache current whitelist
whitelist = self._whitelist.copy()
whitelist = self._list.copy()
# Delegate to attribute setter to run type validations
self.errors = errors
# Join whitelist with previous one
self._whitelist = whitelist | self._whitelist
self._list = whitelist | self._list

def iswhitelisted(self, error):
def isretry(self, error):
"""
Checks if a given error object is whitelisted or not.
Returns:
bool
"""
return error and all([
any(isinstance(error, err) for err in self._whitelist),
return not all([
error is not None,
any(isinstance(error, err) for err in self._list),
getattr(error, '__retry__', False) is False
])


class ErrorBlacklist(ErrorWhitelist):
"""
Provides errors blacklist used to determine those exception errors who
should be retried.
Implements the opposite behaviour to `ErrorWhitelist`.
Arguments:
errors (set|list|tuple[Exception]): optional list of error exceptions
classes to blacklist.
Attributes:
errors (list): list of blacklist errors.
"""

def isretry(self, error):
"""
Checks if a given error object is not whitelisted.
Returns:
bool
"""
return not ErrorWhitelist.isretry(self, error)


def add_whitelist_error(*errors):
"""
Add additional custom errors to the global whitelist.
Expand Down

0 comments on commit 39cad3b

Please sign in to comment.