Skip to content

Commit 288f09c

Browse files
committed
Added jitter and added unit tests
1 parent 1330ad7 commit 288f09c

File tree

2 files changed

+46
-18
lines changed

2 files changed

+46
-18
lines changed

src/databricks/sql/auth/retry.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import random
23
import time
34
import typing
45
from enum import Enum
@@ -287,14 +288,14 @@ def sleep_for_retry(self, response: BaseHTTPResponse) -> bool:
287288
if retry_after:
288289
proposed_wait = retry_after
289290
else:
290-
proposed_wait = self.get_exponential_backoff()
291+
proposed_wait = self.get_backoff_time()
291292

292293
proposed_wait = min(proposed_wait, self.delay_max)
293294
self.check_proposed_wait(proposed_wait)
294295
time.sleep(proposed_wait)
295296
return True
296297

297-
def get_exponential_backoff(self) -> float:
298+
def get_backoff_time(self) -> float:
298299
"""
299300
This method implements the exponential backoff algorithm to calculate the delay between retries.
300301
@@ -306,6 +307,9 @@ def get_exponential_backoff(self) -> float:
306307

307308
current_attempt = self.stop_after_attempts_count - self.total
308309
proposed_backoff = (2**current_attempt) * self.delay_min
310+
if self.backoff_jitter != 0.0:
311+
proposed_backoff += random.random() * self.backoff_jitter
312+
309313
proposed_backoff = min(proposed_backoff, self.delay_max)
310314
self.check_proposed_wait(proposed_backoff)
311315

tests/unit/test_retry.py

+40-16
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
from os import error
22
import time
3-
from unittest.mock import Mock, patch
3+
from unittest.mock import Mock, patch, call
44
import pytest
55
from requests import Request
66
from 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

1011
class 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

Comments
 (0)