adding first stable implementation, reasonable passing test coverage

This commit is contained in:
Ray Holder
2013-01-19 22:56:10 -06:00
parent 987c363146
commit bf2248c2dd
3 changed files with 414 additions and 1 deletions

181
retrying.py Normal file
View File

@@ -0,0 +1,181 @@
## Copyright 2013 Ray Holder
##
## Licensed under the Apache License, Version 2.0 (the "License");
## you may not use this file except in compliance with the License.
## You may obtain a copy of the License at
##
## http://www.apache.org/licenses/LICENSE-2.0
##
## Unless required by applicable law or agreed to in writing, software
## distributed under the License is distributed on an "AS IS" BASIS,
## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
## See the License for the specific language governing permissions and
## limitations under the License.
import random
import sys
import time
def retry(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=100, wait_exponential_max=5000,
retry_on_exception=None, #TODO on_exception
retry_on_result=None): #TODO on_result
def wrap(f):
def wrapped_f(*args, **kw):
return Retrying(
stop=stop,
stop_max_attempt_number=stop_max_attempt_number,
stop_max_delay=stop_max_delay,
wait=wait,
wait_fixed=wait_fixed,
wait_random_min=wait_random_min,
wait_random_max=wait_random_max,
wait_incrementing_start=wait_incrementing_start,
wait_incrementing_increment=wait_incrementing_increment,
wait_exponential_multiplier=wait_exponential_multiplier,
wait_exponential_max=wait_exponential_max,
retry_on_exception=retry_on_exception,
retry_on_result=retry_on_result
).call(f, *args, **kw)
return wrapped_f
return wrap
class Retrying:
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=sys.maxint,
retry_on_exception=None, #TODO on_exception
retry_on_result=None): #TODO on_result
# stop behavior
self.stop = getattr(self, stop)
self._stop_max_attempt_number = stop_max_attempt_number
self._stop_max_delay = stop_max_delay
# control 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
# control retry on exception filter
if retry_on_exception is None:
self._retry_on_exception = self.never_reject
else:
self._retry_on_exception = retry_on_exception
# control retry on result filter
if retry_on_result is None:
self._retry_on_result = self.never_reject
else:
self._retry_on_result = retry_on_result
def never_stop(self, previous_attempt_number, delay_since_first_attempt_ms):
return False
def stop_after_attempt(self, previous_attempt_number, delay_since_first_attempt_ms):
return previous_attempt_number >= self._stop_max_attempt_number
def stop_after_delay(self, previous_attempt_number, delay_since_first_attempt_ms):
return delay_since_first_attempt_ms >= self._stop_max_delay
def no_sleep(self, previous_attempt_number, delay_since_first_attempt_ms):
return 0
def fixed_sleep(self, previous_attempt_number, delay_since_first_attempt_ms):
return self._wait_fixed
def random_sleep(self, previous_attempt_number, delay_since_first_attempt_ms):
return random.randint(self._wait_random_min, self._wait_random_max)
def incrementing_sleep(self, previous_attempt_number, delay_since_first_attempt_ms):
result = self._wait_incrementing_start + (self._wait_incrementing_increment * (previous_attempt_number - 1))
if result < 0:
result = 0
return result
def exponential_sleep(self, previous_attempt_number, delay_since_first_attempt_ms):
exp = 2 ** previous_attempt_number
result = self._wait_exponential_multiplier * exp
if result > self._wait_exponential_max:
result = self._wait_exponential_max
if result < 0:
result = 0
return result
def never_reject(self, result):
return False
def should_reject(self, attempt):
reject = False
if attempt.has_exception:
reject |= self._retry_on_exception(attempt.value)
else:
reject |= self._retry_on_result(attempt.value)
return reject
def call(self, fn, *args, **kwargs):
start_time = int(round(time.time() * 1000))
attempt_number = 1
while True:
try:
attempt = Attempt(fn(*args, **kwargs), False)
except BaseException as e:
attempt = Attempt(e, True)
if not self.should_reject(attempt):
return attempt.get()
# if not attempt.has_exception:
# # filter by retry_on_result() here
# if not self._retry_on_result(attempt.value):
# return attempt.value
# else:
# # filter by retry_on_exception() here
# if not self._retry_on_exception(attempt.value):
# raise RetryError(attempt_number, attempt)
delay_since_first_attempt_ms = int(round(time.time() * 1000)) - start_time
if self.stop(attempt_number, delay_since_first_attempt_ms):
raise RetryError(attempt_number, attempt)
else:
sleep = self.wait(attempt_number, delay_since_first_attempt_ms)
time.sleep(sleep / 1000.0)
attempt_number += 1
class Attempt:
def __init__(self, value, has_exception):
self.value = value
self.has_exception = has_exception
def get(self):
if self.has_exception:
raise self.value
else:
return self.value
class RetryError(Exception):
def __init__(self, failed_attempts, last_attempt):
self.failed_attempts = failed_attempts
self.last_attempt = last_attempt
def __str__(self):
return "Failed attempts: %s, Last attempt: %s" % (str(self.failed_attempts), str(self.last_attempt))

View File

@@ -19,7 +19,7 @@ if sys.argv[-1] == 'publish':
settings.update(
name='retrying',
version='1.0.0',
description='Retry any arbitrary function conditionally via a decorator',
description='Retry any arbitrary function conditionally via a decorator.',
long_description=open('README.rst').read(),
author='Ray Holder',
url='https://github.com/rholder/retrying',

232
test.py Normal file
View File

@@ -0,0 +1,232 @@
## Copyright 2013 Ray Holder
##
## Licensed under the Apache License, Version 2.0 (the "License");
## you may not use this file except in compliance with the License.
## You may obtain a copy of the License at
##
## http://www.apache.org/licenses/LICENSE-2.0
##
## Unless required by applicable law or agreed to in writing, software
## distributed under the License is distributed on an "AS IS" BASIS,
## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
## See the License for the specific language governing permissions and
## limitations under the License.
import time
import unittest
from retrying import RetryError
from retrying import Retrying
from retrying import retry
class TestStopConditions(unittest.TestCase):
def test_never_stop(self):
r = Retrying(stop='never_stop')
self.assertFalse(r.stop(3, 6546))
def test_stop_after_attempt(self):
r = Retrying(stop='stop_after_attempt', 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)
self.assertFalse(r.stop(2, 999))
self.assertTrue(r.stop(2, 1000))
self.assertTrue(r.stop(2, 1001))
class TestWaitConditions(unittest.TestCase):
def test_no_sleep(self):
r = Retrying(wait='no_sleep')
self.assertEqual(0, r.wait(18, 9879))
def test_fixed_sleep(self):
r = Retrying(wait='fixed_sleep', 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)
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)
times = set()
times.add(r.wait(1, 6546))
times.add(r.wait(1, 6546))
times.add(r.wait(1, 6546))
times.add(r.wait(1, 6546))
self.assertTrue(len(times) > 1) # this is kind of non-deterministic...
for t in times:
self.assertTrue(t >= 1000)
self.assertTrue(t <= 2000)
def test_random_sleep_without_min(self):
r = Retrying(wait='random_sleep', wait_random_max=2000)
times = set()
times.add(r.wait(1, 6546))
times.add(r.wait(1, 6546))
times.add(r.wait(1, 6546))
times.add(r.wait(1, 6546))
self.assertTrue(len(times) > 1) # this is kind of non-deterministic...
for t in times:
self.assertTrue(t >= 0)
self.assertTrue(t <= 2000)
def test_exponential(self):
r = Retrying(wait='exponential_sleep')
self.assertEqual(r.wait(1, 0), 2)
self.assertEqual(r.wait(2, 0), 4)
self.assertEqual(r.wait(3, 0), 8)
self.assertEqual(r.wait(4, 0), 16)
self.assertEqual(r.wait(5, 0), 32)
self.assertEqual(r.wait(6, 0), 64)
def test_exponential_with_max_wait(self):
r = Retrying(wait='exponential_sleep', 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)
self.assertEqual(r.wait(4, 0), 16)
self.assertEqual(r.wait(5, 0), 32)
self.assertEqual(r.wait(6, 0), 40)
self.assertEqual(r.wait(7, 0), 40)
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)
self.assertEqual(r.wait(1, 0), 2000)
self.assertEqual(r.wait(2, 0), 4000)
self.assertEqual(r.wait(3, 0), 8000)
self.assertEqual(r.wait(4, 0), 16000)
self.assertEqual(r.wait(5, 0), 32000)
self.assertEqual(r.wait(6, 0), 50000)
self.assertEqual(r.wait(7, 0), 50000)
self.assertEqual(r.wait(50, 0), 50000)
class NoneReturnUntilAfterCount:
"""
This class holds counter state for invoking a method several times in a row.
"""
def __init__(self, count):
self.counter = 0
self.count = count
def go(self):
"""
Return None until after count threshold has been crossed, then return True.
"""
if self.counter < self.count:
self.counter += 1
return None
return True
class NoIOErrorAfterCount:
"""
This class holds counter state for invoking a method several times in a row.
"""
def __init__(self, count):
self.counter = 0
self.count = count
def go(self):
"""
Raise an IOError until after count threshold has been crossed, then return True.
"""
if self.counter < self.count:
self.counter += 1
raise IOError()
return True
class NoNameErrorAfterCount:
"""
This class holds counter state for invoking a method several times in a row.
"""
def __init__(self, count):
self.counter = 0
self.count = count
def go(self):
"""
Raise an NameError until after count threshold has been crossed, then return True.
"""
if self.counter < self.count:
self.counter += 1
raise NameError()
return True
def retry_if_result_none(result):
return result is None
def retry_if_exception_of_type(retryable_types):
def retry_if_exception_these_types(exception):
return isinstance(exception, retryable_types)
return retry_if_exception_these_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)
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)
def _retryable_test_with_stop(thing):
return thing.go()
@retry(retry_on_exception=retry_if_exception_of_type(IOError))
def _retryable_test_with_exception_type_io(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()
class TestDecoratorWrapper(unittest.TestCase):
def test_with_wait(self):
start = current_time_ms()
result = _retryable_test_with_wait(NoneReturnUntilAfterCount(5))
t = current_time_ms() - start
self.assertTrue(t >= 250)
self.assertTrue(result)
def test_with_stop(self):
try:
_retryable_test_with_stop(NoneReturnUntilAfterCount(5))
self.fail("Expected RetryError after 3 attempts")
except RetryError as e:
self.assertEqual(3, e.failed_attempts)
def test_retry_if_exception_of_type(self):
self.assertTrue(_retryable_test_with_exception_type_io(NoIOErrorAfterCount(5)))
try:
_retryable_test_with_exception_type_io(NoNameErrorAfterCount(5))
self.fail("Expected NameError")
except NameError as n:
self.assertTrue(isinstance(n, NameError))
try:
_retryable_test_with_exception_type_io_attempt_limit(NoIOErrorAfterCount(5))
self.fail("RetryError expected")
except RetryError as re:
self.assertEqual(3, re.failed_attempts)
self.assertTrue(re.last_attempt.has_exception)
self.assertTrue(isinstance(re.last_attempt.value, IOError))
#TODO YOU ARE HERE, CONTINUE TESTING
if __name__ == '__main__':
unittest.main()