1
- from os import error
2
1
import time
3
- from unittest .mock import Mock , patch
2
+ from unittest .mock import patch , call
4
3
import pytest
5
- from requests import Request
6
4
from urllib3 import HTTPResponse
7
- from databricks .sql .auth .retry import DatabricksRetryPolicy , RequestHistory
8
-
5
+ from databricks .sql .auth .retry import DatabricksRetryPolicy , RequestHistory , CommandType
6
+ from urllib3 . exceptions import MaxRetryError
9
7
10
8
class TestRetry :
11
9
@pytest .fixture ()
@@ -25,32 +23,55 @@ def error_history(self) -> RequestHistory:
25
23
method = "POST" , url = None , error = None , status = 503 , redirect_location = None
26
24
)
27
25
26
+ def calculate_backoff_time (self , attempt , delay_min , delay_max ):
27
+ exponential_backoff_time = (2 ** attempt ) * delay_min
28
+ return min (exponential_backoff_time , delay_max )
29
+
28
30
@patch ("time.sleep" )
29
31
def test_sleep__no_retry_after (self , t_mock , retry_policy , error_history ):
30
32
retry_policy ._retry_start_time = time .time ()
31
33
retry_policy .history = [error_history , error_history ]
32
34
retry_policy .sleep (HTTPResponse (status = 503 ))
33
- t_mock .assert_called_with (2 )
35
+
36
+ expected_backoff_time = self .calculate_backoff_time (0 , retry_policy .delay_min , retry_policy .delay_max )
37
+ t_mock .assert_called_with (expected_backoff_time )
34
38
35
39
@patch ("time.sleep" )
36
- def test_sleep__retry_after_is_binding (self , t_mock , retry_policy , error_history ):
40
+ def test_sleep__no_retry_after_header__multiple_retries (self , t_mock , retry_policy ):
41
+ num_attempts = retry_policy .stop_after_attempts_count
42
+
37
43
retry_policy ._retry_start_time = time .time ()
38
- retry_policy .history = [error_history , error_history ]
39
- retry_policy .sleep (HTTPResponse (status = 503 , headers = {"Retry-After" : "3" }))
40
- t_mock .assert_called_with (3 )
44
+ retry_policy .command_type = CommandType .OTHER
45
+
46
+ for attempt in range (num_attempts ):
47
+ retry_policy .sleep (HTTPResponse (status = 503 ))
48
+ # Internally urllib3 calls the increment function generating a new instance for every retry
49
+ retry_policy = retry_policy .increment ()
50
+
51
+ expected_backoff_times = []
52
+ for attempt in range (num_attempts ):
53
+ expected_backoff_times .append (self .calculate_backoff_time (attempt , retry_policy .delay_min , retry_policy .delay_max ))
54
+
55
+ # Asserts if the sleep value was called in the expected order
56
+ t_mock .assert_has_calls ([call (expected_time ) for expected_time in expected_backoff_times ])
41
57
42
58
@patch ("time.sleep" )
43
- def test_sleep__retry_after_present_but_not_binding (
44
- self , t_mock , retry_policy , error_history
45
- ):
59
+ def test_excessive_retry_attempts_error (self , t_mock , retry_policy ):
60
+ # Attempting more than stop_after_attempt_count
61
+ num_attempts = retry_policy .stop_after_attempts_count + 1
62
+
46
63
retry_policy ._retry_start_time = time .time ()
47
- retry_policy .history = [error_history , error_history ]
48
- retry_policy .sleep (HTTPResponse (status = 503 , headers = {"Retry-After" : "1" }))
49
- t_mock .assert_called_with (2 )
64
+ retry_policy .command_type = CommandType .OTHER
65
+
66
+ with pytest .raises (MaxRetryError ):
67
+ for attempt in range (num_attempts ):
68
+ retry_policy .sleep (HTTPResponse (status = 503 ))
69
+ # Internally urllib3 calls the increment function generating a new instance for every retry
70
+ retry_policy = retry_policy .increment ()
50
71
51
72
@patch ("time.sleep" )
52
- def test_sleep__retry_after_surpassed (self , t_mock , retry_policy , error_history ):
73
+ def test_sleep__retry_after_present (self , t_mock , retry_policy , error_history ):
53
74
retry_policy ._retry_start_time = time .time ()
54
75
retry_policy .history = [error_history , error_history , error_history ]
55
76
retry_policy .sleep (HTTPResponse (status = 503 , headers = {"Retry-After" : "3" }))
56
- t_mock .assert_called_with (4 )
77
+ t_mock .assert_called_with (3 )
0 commit comments