11from os import error
22import time
3- from unittest .mock import Mock , patch
3+ from unittest .mock import Mock , patch , call
44import pytest
55from requests import Request
66from urllib3 import HTTPResponse
7- from databricks .sql .auth .retry import DatabricksRetryPolicy , RequestHistory
8-
7+ from databricks .sql .auth .retry import DatabricksRetryPolicy , RequestHistory , CommandType
8+ from databricks .sql .exc import MaxRetryDurationError
9+ from urllib3 .exceptions import MaxRetryError
910
1011class TestRetry :
1112 @pytest .fixture ()
@@ -25,32 +26,55 @@ def error_history(self) -> RequestHistory:
2526 method = "POST" , url = None , error = None , status = 503 , redirect_location = None
2627 )
2728
29+ def calculate_backoff_time (self , attempt , delay_min , delay_max ):
30+ exponential_backoff_time = (2 ** attempt ) * delay_min
31+ return min (exponential_backoff_time , delay_max )
32+
2833 @patch ("time.sleep" )
2934 def test_sleep__no_retry_after (self , t_mock , retry_policy , error_history ):
3035 retry_policy ._retry_start_time = time .time ()
3136 retry_policy .history = [error_history , error_history ]
3237 retry_policy .sleep (HTTPResponse (status = 503 ))
33- t_mock .assert_called_with (2 )
38+
39+ expected_backoff_time = self .calculate_backoff_time (0 , retry_policy .delay_min , retry_policy .delay_max )
40+ t_mock .assert_called_with (expected_backoff_time )
3441
3542 @patch ("time.sleep" )
36- def test_sleep__retry_after_is_binding (self , t_mock , retry_policy , error_history ):
43+ def test_sleep__no_retry_after_header__multiple_retries (self , t_mock , retry_policy ):
44+ num_attempts = retry_policy .stop_after_attempts_count
45+
3746 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 )
47+ retry_policy .command_type = CommandType .OTHER
48+
49+ for attempt in range (num_attempts ):
50+ retry_policy .sleep (HTTPResponse (status = 503 ))
51+ # Internally urllib3 calls the increment function generating a new instance for every retry
52+ retry_policy = retry_policy .increment ()
53+
54+ expected_backoff_times = []
55+ for attempt in range (num_attempts ):
56+ expected_backoff_times .append (self .calculate_backoff_time (attempt , retry_policy .delay_min , retry_policy .delay_max ))
57+
58+ # Asserts if the sleep value was called in the expected order
59+ t_mock .assert_has_calls ([call (expected_time ) for expected_time in expected_backoff_times ])
4160
4261 @patch ("time.sleep" )
43- def test_sleep__retry_after_present_but_not_binding (
44- self , t_mock , retry_policy , error_history
45- ):
62+ def test_excessive_retry_attempts_error (self , t_mock , retry_policy ):
63+ # Attempting more than stop_after_attempt_count
64+ num_attempts = retry_policy .stop_after_attempts_count + 1
65+
4666 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 )
67+ retry_policy .command_type = CommandType .OTHER
68+
69+ with pytest .raises (MaxRetryError ):
70+ for attempt in range (num_attempts ):
71+ retry_policy .sleep (HTTPResponse (status = 503 ))
72+ # Internally urllib3 calls the increment function generating a new instance for every retry
73+ retry_policy = retry_policy .increment ()
5074
5175 @patch ("time.sleep" )
52- def test_sleep__retry_after_surpassed (self , t_mock , retry_policy , error_history ):
76+ def test_sleep__retry_after_present (self , t_mock , retry_policy , error_history ):
5377 retry_policy ._retry_start_time = time .time ()
5478 retry_policy .history = [error_history , error_history , error_history ]
5579 retry_policy .sleep (HTTPResponse (status = 503 , headers = {"Retry-After" : "3" }))
56- t_mock .assert_called_with (4 )
80+ t_mock .assert_called_with (3 )
0 commit comments