3147: Add RetryLoop to the Python SDK.
[arvados.git] / sdk / python / arvados / retry.py
1 #!/usr/bin/env python
2
3 import time
4
5 from collections import deque
6
7 import arvados.errors
8
9 class RetryLoop(object):
10     """Coordinate limited retries of code.
11
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:
15
16         loop = RetryLoop(num_retries=2)
17         for tries_left in loop:
18             try:
19                 result = do_something()
20             except TemporaryError as error:
21                 log("error: {} ({} tries left)".format(error, tries_left))
22             else:
23                 loop.save_result(result)
24         if loop.success():
25             return loop.last_result()
26     """
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.
30
31         Arguments:
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.
47         """
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)
54         self._running = None
55         self._success = None
56
57     def __iter__(self):
58         return self
59
60     def running(self):
61         return self._running and (self._success is None)
62
63     def next(self):
64         if self._running is None:
65             self._running = True
66         if (self.tries_left < 1) or not self.running():
67             self._running = False
68             raise StopIteration
69         else:
70             wait_time = max(0, self.next_start_time - time.time())
71             time.sleep(wait_time)
72             self.backoff_wait *= self.backoff_growth
73         self.next_start_time = time.time() + self.backoff_wait
74         self.tries_left -= 1
75         return self.tries_left
76
77     def save_result(self, result):
78         """Record a loop result.
79
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.
83         """
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)
89
90     def success(self):
91         """Return the loop's end state.
92
93         Returns True if the loop obtained a successful result, False if it
94         encountered permanent failure, or else None.
95         """
96         return self._success
97
98     def last_result(self):
99         """Return the most recent result the loop recorded."""
100         try:
101             return self.results[-1]
102         except IndexError:
103             raise arvados.errors.AssertionError(
104                 "queried loop results before any were recorded")