From 924455e0d7a48bd257f9d4389d1cf27d65c20e74 Mon Sep 17 00:00:00 2001 From: Derek Wilson Date: Thu, 15 May 2014 11:18:09 -0700 Subject: [PATCH] make Retrying callable and store stats Because it is useful to do a one-off retry of a function. Additionally, it is useful to be able to get stats from the retrying object - how many attempts were made, what time did the retry start. --- retrying.py | 36 +++++++++++++++++++++++++----------- test_retrying.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/retrying.py b/retrying.py index b337744..032aa69 100644 --- a/retrying.py +++ b/retrying.py @@ -75,18 +75,20 @@ def retry(*dargs, **dkw): """ # support both @retry and @retry() as valid syntax if len(dargs) == 1 and callable(dargs[0]): + retryer = Retrying() def wrap_simple(f): def wrapped_f(*args, **kw): - return Retrying().call(f, *args, **kw) + return retryer(f, *args, **kw) return wrapped_f return wrap_simple(dargs[0]) else: + retryer = Retrying(*dargs, **dkw) def wrap(f): def wrapped_f(*args, **kw): - return Retrying(*dargs, **dkw).call(f, *args, **kw) + return retryer(f, *args, **kw) return wrapped_f @@ -168,6 +170,17 @@ def __init__(self, self._wrap_exception = wrap_exception + self._attempt_number = 0 + self._start_time = None + + @property + def attempts(self): + return self._attempt_number + + @property + def start_time(self): + return self._start_time + 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 @@ -222,27 +235,28 @@ def should_reject(self, attempt): return reject - def call(self, fn, *args, **kwargs): - start_time = int(round(time.time() * 1000)) - attempt_number = 1 + def __call__(self, fn, *args, **kwargs): + self._start_time = int(round(time.time() * 1000)) + self._attempt_number = 1 while True: try: - attempt = Attempt(fn(*args, **kwargs), attempt_number, False) + attempt = Attempt(fn(*args, **kwargs), self._attempt_number, False) except: tb = sys.exc_info() - attempt = Attempt(tb, attempt_number, True) + attempt = Attempt(tb, self._attempt_number, True) if not self.should_reject(attempt): return attempt.get(self._wrap_exception) - delay_since_first_attempt_ms = int(round(time.time() * 1000)) - start_time - if self.stop(attempt_number, delay_since_first_attempt_ms): + delay_since_first_attempt_ms = int(round(time.time() * 1000)) - self._start_time + if self.stop(self._attempt_number, delay_since_first_attempt_ms): raise RetryError(attempt) + else: - sleep = self.wait(attempt_number, delay_since_first_attempt_ms) + sleep = self.wait(self._attempt_number, delay_since_first_attempt_ms) time.sleep(sleep / 1000.0) - attempt_number += 1 + self._attempt_number += 1 class Attempt(object): """ diff --git a/test_retrying.py b/test_retrying.py index 43c9000..d2fa1f0 100644 --- a/test_retrying.py +++ b/test_retrying.py @@ -387,5 +387,33 @@ def test_defaults(self): self.assertTrue(_retryable_default(NoCustomErrorAfterCount(5))) self.assertTrue(_retryable_default_f(NoCustomErrorAfterCount(5))) + +class TestDirectUsage(unittest.TestCase): + def test_direct_call(self): + value = 1 + def working(): + return value + + r = Retrying() + self.assertFalse(r.start_time) + self.assertFalse(r.attempts) + result = r(working) + self.assertEqual(value, result) + self.assertEqual(1, r.attempts) + + def test_direct_call_with_args(self): + value = 1 + def working(value): + return value + + r = Retrying() + self.assertFalse(r.start_time) + self.assertFalse(r.attempts) + result = r(working, value) + self.assertEqual(value, result) + self.assertEqual(1, r.attempts) + self.assertTrue(r.start_time) + + if __name__ == '__main__': unittest.main()