5 from collections import deque
9 _HTTP_SUCCESSES = set(xrange(200, 300))
10 _HTTP_CAN_RETRY = set([408, 409, 422, 423, 500, 502, 503, 504])
12 class RetryLoop(object):
13 """Coordinate limited retries of code.
15 RetryLoop coordinates a loop that runs until it records a
16 successful result or tries too many times, whichever comes first.
17 Typical use looks like:
19 loop = RetryLoop(num_retries=2)
20 for tries_left in loop:
22 result = do_something()
23 except TemporaryError as error:
24 log("error: {} ({} tries left)".format(error, tries_left))
26 loop.save_result(result)
28 return loop.last_result()
30 def __init__(self, num_retries, success_check=lambda r: True,
31 backoff_start=0, backoff_growth=2, save_results=1):
32 """Construct a new RetryLoop.
35 * num_retries: The maximum number of times to retry the loop if it
36 doesn't succeed. This means the loop could run at most 1+N times.
37 * success_check: This is a function that will be called each
38 time the loop saves a result. The function should return
39 True if the result indicates loop success, False if it
40 represents a permanent failure state, and None if the loop
41 should continue. If no function is provided, the loop will
42 end as soon as it records any result.
43 * backoff_start: The number of seconds that must pass before the
44 loop's second iteration. Default 0, which disables all waiting.
45 * backoff_growth: The wait time multiplier after each iteration.
46 Default 2 (i.e., double the wait time each time).
47 * save_results: Specify a number to save the last N results
48 that the loop recorded. These records are available through
49 the results attribute, oldest first. Default 1.
51 self.tries_left = num_retries + 1
52 self.check_result = success_check
53 self.backoff_wait = backoff_start
54 self.backoff_growth = backoff_growth
55 self.next_start_time = 0
56 self.results = deque(maxlen=save_results)
64 return self._running and (self._success is None)
67 if self._running is None:
69 if (self.tries_left < 1) or not self.running():
73 wait_time = max(0, self.next_start_time - time.time())
75 self.backoff_wait *= self.backoff_growth
76 self.next_start_time = time.time() + self.backoff_wait
78 return self.tries_left
80 def save_result(self, result):
81 """Record a loop result.
83 Save the given result, and end the loop if it indicates
84 success or permanent failure. See __init__'s documentation
85 about success_check to learn how to make that indication.
87 if not self.running():
88 raise arvados.errors.AssertionError(
89 "recorded a loop result after the loop finished")
90 self.results.append(result)
91 self._success = self.check_result(result)
94 """Return the loop's end state.
96 Returns True if the loop obtained a successful result, False if it
97 encountered permanent failure, or else None.
101 def last_result(self):
102 """Return the most recent result the loop recorded."""
104 return self.results[-1]
106 raise arvados.errors.AssertionError(
107 "queried loop results before any were recorded")
110 def check_http_response_success(result):
111 """Convert an httplib2 request result to a loop control flag.
113 Pass this method the 2-tuple returned by httplib2.Http.request. It
114 returns True if the response indicates success, None if it indicates
115 temporary failure, and False otherwise. You can use this as the
116 success_check for a RetryLoop.
118 Implementation details:
119 * Any 2xx result returns True.
120 * A select few status codes, or any malformed responses, return None.
121 422 Unprocessable Entity is in this category. This may not meet the
122 letter of the HTTP specification, but the Arvados API server will
123 use it for various server-side problems like database connection
125 * Everything else returns False. Note that this includes 1xx and
126 3xx status codes. They don't indicate success, and you can't
127 retry those requests verbatim.
130 status = int(result[0].status)
133 if status in _HTTP_SUCCESSES:
135 elif status in _HTTP_CAN_RETRY:
137 elif 100 <= status < 600:
140 return None # Get well soon, server.