8 from collections import deque
12 _HTTP_SUCCESSES = set(xrange(200, 300))
13 _HTTP_CAN_RETRY = set([408, 409, 422, 423, 500, 502, 503, 504])
15 class RetryLoop(object):
16 """Coordinate limited retries of code.
18 RetryLoop coordinates a loop that runs until it records a
19 successful result or tries too many times, whichever comes first.
20 Typical use looks like:
22 loop = RetryLoop(num_retries=2)
23 for tries_left in loop:
25 result = do_something()
26 except TemporaryError as error:
27 log("error: {} ({} tries left)".format(error, tries_left))
29 loop.save_result(result)
31 return loop.last_result()
33 def __init__(self, num_retries, success_check=lambda r: True,
34 backoff_start=0, backoff_growth=2, save_results=1):
35 """Construct a new RetryLoop.
38 * num_retries: The maximum number of times to retry the loop if it
39 doesn't succeed. This means the loop could run at most 1+N times.
40 * success_check: This is a function that will be called each
41 time the loop saves a result. The function should return
42 True if the result indicates loop success, False if it
43 represents a permanent failure state, and None if the loop
44 should continue. If no function is provided, the loop will
45 end as soon as it records any result.
46 * backoff_start: The number of seconds that must pass before the
47 loop's second iteration. Default 0, which disables all waiting.
48 * backoff_growth: The wait time multiplier after each iteration.
49 Default 2 (i.e., double the wait time each time).
50 * save_results: Specify a number to save the last N results
51 that the loop recorded. These records are available through
52 the results attribute, oldest first. Default 1.
54 self.tries_left = num_retries + 1
55 self.check_result = success_check
56 self.backoff_wait = backoff_start
57 self.backoff_growth = backoff_growth
58 self.next_start_time = 0
59 self.results = deque(maxlen=save_results)
67 return self._running and (self._success is None)
70 if self._running is None:
72 if (self.tries_left < 1) or not self.running():
76 wait_time = max(0, self.next_start_time - time.time())
78 self.backoff_wait *= self.backoff_growth
79 self.next_start_time = time.time() + self.backoff_wait
81 return self.tries_left
83 def save_result(self, result):
84 """Record a loop result.
86 Save the given result, and end the loop if it indicates
87 success or permanent failure. See __init__'s documentation
88 about success_check to learn how to make that indication.
90 if not self.running():
91 raise arvados.errors.AssertionError(
92 "recorded a loop result after the loop finished")
93 self.results.append(result)
94 self._success = self.check_result(result)
97 """Return the loop's end state.
99 Returns True if the loop obtained a successful result, False if it
100 encountered permanent failure, or else None.
104 def last_result(self):
105 """Return the most recent result the loop recorded."""
107 return self.results[-1]
109 raise arvados.errors.AssertionError(
110 "queried loop results before any were recorded")
113 def check_http_response_success(status_code):
114 """Convert an HTTP status code to a loop control flag.
116 Pass this method a numeric HTTP status code. It returns True if
117 the code indicates success, None if it indicates temporary
118 failure, and False otherwise. You can use this as the
119 success_check for a RetryLoop.
121 Implementation details:
122 * Any 2xx result returns True.
123 * A select few status codes, or any malformed responses, return None.
124 422 Unprocessable Entity is in this category. This may not meet the
125 letter of the HTTP specification, but the Arvados API server will
126 use it for various server-side problems like database connection
128 * Everything else returns False. Note that this includes 1xx and
129 3xx status codes. They don't indicate success, and you can't
130 retry those requests verbatim.
132 if status_code in _HTTP_SUCCESSES:
134 elif status_code in _HTTP_CAN_RETRY:
136 elif 100 <= status_code < 600:
139 return None # Get well soon, server.
141 def retry_method(orig_func):
142 """Provide a default value for a method's num_retries argument.
144 This is a decorator for instance and class methods that accept a
145 num_retries argument, with a None default. When the method is called
146 without a value for num_retries, it will be set from the underlying
147 instance or class' num_retries attribute.
149 @functools.wraps(orig_func)
150 def num_retries_setter(self, *args, **kwargs):
151 arg_vals = inspect.getcallargs(orig_func, self, *args, **kwargs)
152 if arg_vals['num_retries'] is None:
153 kwargs['num_retries'] = self.num_retries
154 return orig_func(self, *args, **kwargs)
155 return num_retries_setter