1- from os import error
21import time
3- from unittest .mock import Mock , patch
2+ from unittest .mock import patch , call
43import pytest
5- from requests import Request
64from 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
97
108class TestRetry :
119 @pytest .fixture ()
@@ -25,32 +23,55 @@ def error_history(self) -> RequestHistory:
2523 method = "POST" , url = None , error = None , status = 503 , redirect_location = None
2624 )
2725
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+
2830 @patch ("time.sleep" )
2931 def test_sleep__no_retry_after (self , t_mock , retry_policy , error_history ):
3032 retry_policy ._retry_start_time = time .time ()
3133 retry_policy .history = [error_history , error_history ]
3234 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 )
3438
3539 @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+
3743 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 ])
4157
4258 @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+
4663 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 ()
5071
5172 @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 ):
5374 retry_policy ._retry_start_time = time .time ()
5475 retry_policy .history = [error_history , error_history , error_history ]
5576 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