5 from collections import deque
9 class RetryLoop(object):
10 """Coordinate limited retries of code.
12 RetryLoop coordinates a loop that runs until it records a
13 successful result or tries too many times, whichever comes first.
14 Typical use looks like:
16 loop = RetryLoop(num_retries=2)
17 for tries_left in loop:
19 result = do_something()
20 except TemporaryError as error:
21 log("error: {} ({} tries left)".format(error, tries_left))
23 loop.save_result(result)
25 return loop.last_result()
27 def __init__(self, num_retries, success_check=lambda r: True,
28 backoff_start=0, backoff_growth=2, save_results=1):
29 """Construct a new RetryLoop.
32 * num_retries: The maximum number of times to retry the loop if it
33 doesn't succeed. This means the loop could run at most 1+N times.
34 * success_check: This is a function that will be called each
35 time the loop saves a result. The function should return
36 True if the result indicates loop success, False if it
37 represents a permanent failure state, and None if the loop
38 should continue. If no function is provided, the loop will
39 end as soon as it records any result.
40 * backoff_start: The number of seconds that must pass before the
41 loop's second iteration. Default 0, which disables all waiting.
42 * backoff_growth: The wait time multiplier after each iteration.
43 Default 2 (i.e., double the wait time each time).
44 * save_results: Specify a number to save the last N results
45 that the loop recorded. These records are available through
46 the results attribute, oldest first. Default 1.
48 self.tries_left = num_retries + 1
49 self.check_result = success_check
50 self.backoff_wait = backoff_start
51 self.backoff_growth = backoff_growth
52 self.next_start_time = 0
53 self.results = deque(maxlen=save_results)
61 return self._running and (self._success is None)
64 if self._running is None:
66 if (self.tries_left < 1) or not self.running():
70 wait_time = max(0, self.next_start_time - time.time())
72 self.backoff_wait *= self.backoff_growth
73 self.next_start_time = time.time() + self.backoff_wait
75 return self.tries_left
77 def save_result(self, result):
78 """Record a loop result.
80 Save the given result, and end the loop if it indicates
81 success or permanent failure. See __init__'s documentation
82 about success_check to learn how to make that indication.
84 if not self.running():
85 raise arvados.errors.AssertionError(
86 "recorded a loop result after the loop finished")
87 self.results.append(result)
88 self._success = self.check_result(result)
91 """Return the loop's end state.
93 Returns True if the loop obtained a successful result, False if it
94 encountered permanent failure, or else None.
98 def last_result(self):
99 """Return the most recent result the loop recorded."""
101 return self.results[-1]
103 raise arvados.errors.AssertionError(
104 "queried loop results before any were recorded")