Merge branch 'master' into 6827-no-passwords-in-logs
[arvados.git] / sdk / python / tests / test_retry.py
1 #!/usr/bin/env python
2
3 import itertools
4 import unittest
5
6 import arvados.errors as arv_error
7 import arvados.retry as arv_retry
8 import mock
9
10 class RetryLoopTestMixin(object):
11     @staticmethod
12     def loop_success(result):
13         # During the tests, we use integers that look like HTTP status
14         # codes as loop results.  Then we define simplified HTTP
15         # heuristics here to decide whether the result is success (True),
16         # permanent failure (False), or temporary failure (None).
17         if result < 400:
18             return True
19         elif result < 500:
20             return False
21         else:
22             return None
23
24     def run_loop(self, num_retries, *results, **kwargs):
25         responses = itertools.chain(results, itertools.repeat(None))
26         retrier = arv_retry.RetryLoop(num_retries, self.loop_success,
27                                       **kwargs)
28         for tries_left, response in itertools.izip(retrier, responses):
29             retrier.save_result(response)
30         return retrier
31
32     def check_result(self, retrier, expect_success, last_code):
33         self.assertIs(retrier.success(), expect_success,
34                       "loop success flag is incorrect")
35         self.assertEqual(last_code, retrier.last_result())
36
37
38 class RetryLoopTestCase(unittest.TestCase, RetryLoopTestMixin):
39     def test_zero_retries_and_success(self):
40         retrier = self.run_loop(0, 200)
41         self.check_result(retrier, True, 200)
42
43     def test_zero_retries_and_tempfail(self):
44         retrier = self.run_loop(0, 500, 501)
45         self.check_result(retrier, None, 500)
46
47     def test_zero_retries_and_permfail(self):
48         retrier = self.run_loop(0, 400, 201)
49         self.check_result(retrier, False, 400)
50
51     def test_one_retry_with_immediate_success(self):
52         retrier = self.run_loop(1, 200, 201)
53         self.check_result(retrier, True, 200)
54
55     def test_one_retry_with_delayed_success(self):
56         retrier = self.run_loop(1, 500, 201)
57         self.check_result(retrier, True, 201)
58
59     def test_one_retry_with_no_success(self):
60         retrier = self.run_loop(1, 500, 501, 502)
61         self.check_result(retrier, None, 501)
62
63     def test_one_retry_but_permfail(self):
64         retrier = self.run_loop(1, 400, 201)
65         self.check_result(retrier, False, 400)
66
67     def test_two_retries_with_immediate_success(self):
68         retrier = self.run_loop(2, 200, 201, 202)
69         self.check_result(retrier, True, 200)
70
71     def test_two_retries_with_success_after_one(self):
72         retrier = self.run_loop(2, 500, 201, 502)
73         self.check_result(retrier, True, 201)
74
75     def test_two_retries_with_success_after_two(self):
76         retrier = self.run_loop(2, 500, 501, 202, 503)
77         self.check_result(retrier, True, 202)
78
79     def test_two_retries_with_no_success(self):
80         retrier = self.run_loop(2, 500, 501, 502, 503)
81         self.check_result(retrier, None, 502)
82
83     def test_two_retries_with_permfail(self):
84         retrier = self.run_loop(2, 500, 401, 202)
85         self.check_result(retrier, False, 401)
86
87     def test_save_result_before_start_is_error(self):
88         retrier = arv_retry.RetryLoop(0)
89         self.assertRaises(arv_error.AssertionError, retrier.save_result, 1)
90
91     def test_save_result_after_end_is_error(self):
92         retrier = arv_retry.RetryLoop(0)
93         for count in retrier:
94             pass
95         self.assertRaises(arv_error.AssertionError, retrier.save_result, 1)
96
97
98 @mock.patch('time.time', side_effect=itertools.count())
99 @mock.patch('time.sleep')
100 class RetryLoopBackoffTestCase(unittest.TestCase, RetryLoopTestMixin):
101     def run_loop(self, num_retries, *results, **kwargs):
102         kwargs.setdefault('backoff_start', 8)
103         return super(RetryLoopBackoffTestCase, self).run_loop(
104             num_retries, *results, **kwargs)
105
106     def check_backoff(self, sleep_mock, sleep_count, multiplier=1):
107         # Figure out how much time we actually spent sleeping.
108         sleep_times = [arglist[0][0] for arglist in sleep_mock.call_args_list
109                        if arglist[0][0] > 0]
110         self.assertEqual(sleep_count, len(sleep_times),
111                          "loop did not back off correctly")
112         last_wait = 0
113         for this_wait in sleep_times:
114             self.assertGreater(this_wait, last_wait * multiplier,
115                                "loop did not grow backoff times correctly")
116             last_wait = this_wait
117
118     def test_no_backoff_with_no_retries(self, sleep_mock, time_mock):
119         self.run_loop(0, 500, 201)
120         self.check_backoff(sleep_mock, 0)
121
122     def test_no_backoff_after_success(self, sleep_mock, time_mock):
123         self.run_loop(1, 200, 501)
124         self.check_backoff(sleep_mock, 0)
125
126     def test_no_backoff_after_permfail(self, sleep_mock, time_mock):
127         self.run_loop(1, 400, 201)
128         self.check_backoff(sleep_mock, 0)
129
130     def test_backoff_before_success(self, sleep_mock, time_mock):
131         self.run_loop(5, 500, 501, 502, 203, 504)
132         self.check_backoff(sleep_mock, 3)
133
134     def test_backoff_before_permfail(self, sleep_mock, time_mock):
135         self.run_loop(5, 500, 501, 502, 403, 504)
136         self.check_backoff(sleep_mock, 3)
137
138     def test_backoff_all_tempfail(self, sleep_mock, time_mock):
139         self.run_loop(3, 500, 501, 502, 503, 504)
140         self.check_backoff(sleep_mock, 3)
141
142     def test_backoff_multiplier(self, sleep_mock, time_mock):
143         self.run_loop(5, 500, 501, 502, 503, 504, 505,
144                       backoff_start=5, backoff_growth=10)
145         self.check_backoff(sleep_mock, 5, 9)
146
147
148 class CheckHTTPResponseSuccessTestCase(unittest.TestCase):
149     def results_map(self, *codes):
150         for code in codes:
151             yield code, arv_retry.check_http_response_success(code)
152
153     def check(assert_name):
154         def check_method(self, expected, *codes):
155             assert_func = getattr(self, assert_name)
156             for code, actual in self.results_map(*codes):
157                 assert_func(expected, actual,
158                             "{} status flagged {}".format(code, actual))
159                 if assert_name != 'assertIs':
160                     self.assertTrue(
161                         actual is True or actual is False or actual is None,
162                         "{} status returned {}".format(code, actual))
163         return check_method
164
165     check_is = check('assertIs')
166     check_is_not = check('assertIsNot')
167
168     def test_obvious_successes(self):
169         self.check_is(True, *range(200, 207))
170
171     def test_obvious_stops(self):
172         self.check_is(False, 424, 426, 428, 431,
173                       *range(400, 408) + range(410, 420))
174
175     def test_obvious_retries(self):
176         self.check_is(None, 500, 502, 503, 504)
177
178     def test_4xx_retries(self):
179         self.check_is(None, 408, 409, 422, 423)
180
181     def test_5xx_failures(self):
182         self.check_is(False, 501, *range(505, 512))
183
184     def test_1xx_not_retried(self):
185         self.check_is_not(None, 100, 101)
186
187     def test_redirects_not_retried(self):
188         self.check_is_not(None, *range(300, 309))
189
190     def test_wacky_code_retries(self):
191         self.check_is(None, 0, 99, 600, -200)
192
193
194 class RetryMethodTestCase(unittest.TestCase):
195     class Tester(object):
196         def __init__(self):
197             self.num_retries = 1
198
199         @arv_retry.retry_method
200         def check(self, a, num_retries=None, z=0):
201             return (a, num_retries, z)
202
203
204     def test_positional_arg_raises(self):
205         # unsupported use -- make sure we raise rather than ignore
206         with self.assertRaises(TypeError):
207             self.assertEqual((3, 2, 0), self.Tester().check(3, 2))
208
209     def test_keyword_arg_passed(self):
210         self.assertEqual((4, 3, 0), self.Tester().check(num_retries=3, a=4))
211
212     def test_not_specified(self):
213         self.assertEqual((0, 1, 0), self.Tester().check(0))
214
215     def test_not_specified_with_other_kwargs(self):
216         self.assertEqual((1, 1, 1), self.Tester().check(1, z=1))
217
218     def test_bad_call(self):
219         with self.assertRaises(TypeError):
220             self.Tester().check(num_retries=2)
221
222
223 if __name__ == '__main__':
224     unittest.main()