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