Merge pull request #3 from underrun/master

remove need for specification of stop/wait type
This commit is contained in:
Ray Holder 2014-05-05 03:15:21 +02:00
commit 389d407f9d
4 changed files with 82 additions and 58 deletions

View File

@ -13,3 +13,4 @@ Patches and Suggestions
- Anthony McClosky
- Jason Dunkelberger
- Justin Turner Arthur
- J Derek Wilson

View File

@ -75,7 +75,7 @@ Let's be a little less persistent and set some boundaries, such as the number of
.. code-block:: python
@retry(stop='stop_after_attempt', stop_max_attempt_number=7)
@retry(stop_max_attempt_number=7)
def stop_after_7_attempts():
print "Stopping after 7 attempts"
@ -83,7 +83,7 @@ We don't have all day, so let's set a boundary for how long we should be retryin
.. code-block:: python
@retry(stop='stop_after_delay', stop_max_delay=10000)
@retry(stop_max_delay=10000)
def stop_after_10_s():
print "Stopping after 10 seconds"
@ -91,7 +91,7 @@ Most things don't like to be polled as fast as possible, so let's just wait 2 se
.. code-block:: python
@retry(wait='fixed_sleep', wait_fixed=2000)
@retry(wait_fixed=2000)
def wait_2_s():
print "Wait 2 second between retries"
@ -100,7 +100,7 @@ Some things perform best with a bit of randomness injected.
.. code-block:: python
@retry(wait='random_sleep', wait_random_min=1000, wait_random_max=2000)
@retry(wait_random_min=1000, wait_random_max=2000)
def wait_random_1_to_2_s():
print "Randomly wait 1 to 2 seconds between retries"
@ -108,7 +108,7 @@ Then again, it's hard to beat exponential backoff when retrying distributed serv
.. code-block:: python
@retry(wait='exponential_sleep', wait_exponential_multiplier=1000, wait_exponential_max=10000)
@retry(wait_exponential_multiplier=1000, wait_exponential_max=10000)
def wait_exponential_1000():
print "Wait 2^x * 1000 milliseconds between each retry, up to 10 seconds, then 10 seconds afterwards"

View File

@ -73,54 +73,85 @@ def retry(*dargs, **dkw):
"""
TODO comment
"""
def wrap(f):
def wrapped_f(*args, **kw):
return Retrying(*dargs, **dkw).call(f, *args, **kw)
return wrapped_f
def wrap_simple(f):
def wrapped_f(*args, **kw):
return Retrying().call(f, *args, **kw)
return wrapped_f
# support both @retry and @retry() as valid syntax
if len(dargs) == 1 and callable(dargs[0]):
def wrap_simple(f):
def wrapped_f(*args, **kw):
return Retrying().call(f, *args, **kw)
return wrapped_f
return wrap_simple(dargs[0])
else:
def wrap(f):
def wrapped_f(*args, **kw):
return Retrying(*dargs, **dkw).call(f, *args, **kw)
return wrapped_f
return wrap
class Retrying:
class Retrying(object):
def __init__(self,
stop='never_stop',
stop_max_attempt_number=5,
stop_max_delay=100,
wait='no_sleep',
wait_fixed=1000,
wait_random_min=0, wait_random_max=1000,
wait_incrementing_start=0, wait_incrementing_increment=100,
wait_exponential_multiplier=1, wait_exponential_max=MAX_WAIT,
stop=None, wait=None,
stop_max_attempt_number=None,
stop_max_delay=None,
wait_fixed=None,
wait_random_min=None, wait_random_max=None,
wait_incrementing_start=None, wait_incrementing_increment=None,
wait_exponential_multiplier=None, wait_exponential_max=None,
retry_on_exception=None,
retry_on_result=None,
wrap_exception=False):
self._stop_max_attempt_number = 5 if stop_max_attempt_number is None else stop_max_attempt_number
self._stop_max_delay = 100 if stop_max_delay is None else stop_max_delay
self._wait_fixed = 1000 if wait_fixed is None else wait_fixed
self._wait_random_min = 0 if wait_random_min is None else wait_random_min
self._wait_random_max = 1000 if wait_random_max is None else wait_random_max
self._wait_incrementing_start = 0 if wait_incrementing_start is None else wait_incrementing_start
self._wait_incrementing_increment = 100 if wait_incrementing_increment is None else wait_incrementing_increment
self._wait_exponential_multiplier = 1 if wait_exponential_multiplier is None else wait_exponential_multiplier
self._wait_exponential_max = MAX_WAIT if wait_exponential_max is None else wait_exponential_max
# TODO add chaining of stop behaviors
# stop behavior
self.stop = getattr(self, stop)
self._stop_max_attempt_number = stop_max_attempt_number
self._stop_max_delay = stop_max_delay
stop_funcs = []
if stop_max_attempt_number is not None:
stop_funcs.append(self.stop_after_attempt)
if stop_max_delay is not None:
stop_funcs.append(self.stop_after_delay)
if stop is None:
self.stop = lambda attempts, delay: any(f(attempts, delay) for f in stop_funcs)
else:
self.stop = getattr(self, stop)
# TODO add chaining of wait behaviors
# wait behavior
self.wait = getattr(self, wait)
self._wait_fixed = wait_fixed
self._wait_random_min = wait_random_min
self._wait_random_max = wait_random_max
self._wait_incrementing_start = wait_incrementing_start
self._wait_incrementing_increment = wait_incrementing_increment
self._wait_exponential_multiplier = wait_exponential_multiplier
self._wait_exponential_max = wait_exponential_max
wait_funcs = [lambda *args, **kwargs: 0]
if wait_fixed is not None:
wait_funcs.append(self.fixed_sleep)
if wait_random_min is not None or wait_random_max is not None:
wait_funcs.append(self.random_sleep)
if wait_incrementing_start is not None or wait_incrementing_increment is not None:
wait_funcs.append(self.incrementing_sleep)
if wait_exponential_multiplier is not None or wait_exponential_max is not None:
wait_funcs.append(self.exponential_sleep)
if wait is None:
self.wait = lambda attempts, delay: max(f(attempts, delay) for f in wait_funcs)
else:
self.wait = self.getattr(wait)
# retry on exception filter
if retry_on_exception is None:
@ -137,10 +168,6 @@ class Retrying:
self._wrap_exception = wrap_exception
def never_stop(self, previous_attempt_number, delay_since_first_attempt_ms):
"""Never stop retrying."""
return False
def stop_after_attempt(self, previous_attempt_number, delay_since_first_attempt_ms):
"""Stop after the previous attempt >= stop_max_attempt_number."""
return previous_attempt_number >= self._stop_max_attempt_number
@ -217,7 +244,7 @@ class Retrying:
attempt_number += 1
class Attempt:
class Attempt(object):
"""
An Attempt encapsulates a call to a target function that may end as a
normal return value from the function or an Exception depending on what

View File

@ -22,17 +22,17 @@ from retrying import retry
class TestStopConditions(unittest.TestCase):
def test_never_stop(self):
r = Retrying(stop='never_stop')
r = Retrying()
self.assertFalse(r.stop(3, 6546))
def test_stop_after_attempt(self):
r = Retrying(stop='stop_after_attempt', stop_max_attempt_number=3)
r = Retrying(stop_max_attempt_number=3)
self.assertFalse(r.stop(2, 6546))
self.assertTrue(r.stop(3, 6546))
self.assertTrue(r.stop(4, 6546))
def test_stop_after_delay(self):
r = Retrying(stop='stop_after_delay', stop_max_delay=1000)
r = Retrying(stop_max_delay=1000)
self.assertFalse(r.stop(2, 999))
self.assertTrue(r.stop(2, 1000))
self.assertTrue(r.stop(2, 1001))
@ -40,21 +40,21 @@ class TestStopConditions(unittest.TestCase):
class TestWaitConditions(unittest.TestCase):
def test_no_sleep(self):
r = Retrying(wait='no_sleep')
r = Retrying()
self.assertEqual(0, r.wait(18, 9879))
def test_fixed_sleep(self):
r = Retrying(wait='fixed_sleep', wait_fixed=1000)
r = Retrying(wait_fixed=1000)
self.assertEqual(1000, r.wait(12, 6546))
def test_incrementing_sleep(self):
r = Retrying(wait='incrementing_sleep', wait_incrementing_start=500, wait_incrementing_increment=100)
r = Retrying(wait_incrementing_start=500, wait_incrementing_increment=100)
self.assertEqual(500, r.wait(1, 6546))
self.assertEqual(600, r.wait(2, 6546))
self.assertEqual(700, r.wait(3, 6546))
def test_random_sleep(self):
r = Retrying(wait='random_sleep', wait_random_min=1000, wait_random_max=2000)
r = Retrying(wait_random_min=1000, wait_random_max=2000)
times = set()
times.add(r.wait(1, 6546))
times.add(r.wait(1, 6546))
@ -66,7 +66,7 @@ class TestWaitConditions(unittest.TestCase):
self.assertTrue(t <= 2000)
def test_random_sleep_without_min(self):
r = Retrying(wait='random_sleep', wait_random_max=2000)
r = Retrying(wait_random_max=2000)
times = set()
times.add(r.wait(1, 6546))
times.add(r.wait(1, 6546))
@ -78,7 +78,7 @@ class TestWaitConditions(unittest.TestCase):
self.assertTrue(t <= 2000)
def test_exponential(self):
r = Retrying(wait='exponential_sleep')
r = Retrying(wait_exponential_max=100000)
self.assertEqual(r.wait(1, 0), 2)
self.assertEqual(r.wait(2, 0), 4)
self.assertEqual(r.wait(3, 0), 8)
@ -87,7 +87,7 @@ class TestWaitConditions(unittest.TestCase):
self.assertEqual(r.wait(6, 0), 64)
def test_exponential_with_max_wait(self):
r = Retrying(wait='exponential_sleep', wait_exponential_max=40)
r = Retrying(wait_exponential_max=40)
self.assertEqual(r.wait(1, 0), 2)
self.assertEqual(r.wait(2, 0), 4)
self.assertEqual(r.wait(3, 0), 8)
@ -98,7 +98,7 @@ class TestWaitConditions(unittest.TestCase):
self.assertEqual(r.wait(50, 0), 40)
def test_exponential_with_max_wait_and_multiplier(self):
r = Retrying(wait='exponential_sleep', wait_exponential_max=50000, wait_exponential_multiplier=1000)
r = Retrying(wait_exponential_max=50000, wait_exponential_multiplier=1000)
self.assertEqual(r.wait(1, 0), 2000)
self.assertEqual(r.wait(2, 0), 4000)
self.assertEqual(r.wait(3, 0), 8000)
@ -208,11 +208,11 @@ def retry_if_exception_of_type(retryable_types):
def current_time_ms():
return int(round(time.time() * 1000))
@retry(wait='fixed_sleep', wait_fixed=50, retry_on_result=retry_if_result_none)
@retry(wait_fixed=50, retry_on_result=retry_if_result_none)
def _retryable_test_with_wait(thing):
return thing.go()
@retry(stop='stop_after_attempt', stop_max_attempt_number=3, retry_on_result=retry_if_result_none)
@retry(stop_max_attempt_number=3, retry_on_result=retry_if_result_none)
def _retryable_test_with_stop(thing):
return thing.go()
@ -225,14 +225,12 @@ def _retryable_test_with_exception_type_io_wrap(thing):
return thing.go()
@retry(
stop='stop_after_attempt',
stop_max_attempt_number=3,
retry_on_exception=retry_if_exception_of_type(IOError))
def _retryable_test_with_exception_type_io_attempt_limit(thing):
return thing.go()
@retry(
stop='stop_after_attempt',
stop_max_attempt_number=3,
retry_on_exception=retry_if_exception_of_type(IOError),
wrap_exception=True)
@ -256,14 +254,12 @@ def _retryable_test_with_exception_type_custom_wrap(thing):
return thing.go()
@retry(
stop='stop_after_attempt',
stop_max_attempt_number=3,
retry_on_exception=retry_if_exception_of_type(CustomError))
def _retryable_test_with_exception_type_custom_attempt_limit(thing):
return thing.go()
@retry(
stop='stop_after_attempt',
stop_max_attempt_number=3,
retry_on_exception=retry_if_exception_of_type(CustomError),
wrap_exception=True)
@ -383,4 +379,4 @@ class TestDecoratorWrapper(unittest.TestCase):
self.assertTrue(_retryable_default_f(NoCustomErrorAfterCount(5)))
if __name__ == '__main__':
unittest.main()
unittest.main()