Skip to content

Commit 2977f70

Browse files
authored
Reverting retry behavior on 429s/503s to how it worked in 2.9.3 (#349)
Signed-off-by: Ben Cassell <[email protected]>
1 parent e594eb1 commit 2977f70

File tree

4 files changed

+112
-8
lines changed

4 files changed

+112
-8
lines changed

CHANGELOG.md

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Release History
22

3+
# 3.0.4 (TBD)
4+
5+
- Revert retry-after behavior to be exponential backoff
6+
37
# 3.0.3 (2024-02-02)
48

59
- Add support in-house OAuth on GCP (#338)
@@ -50,15 +54,15 @@
5054

5155
## 2.9.2 (2023-08-17)
5256

53-
__Note: this release was yanked from Pypi on 13 September 2023 due to compatibility issues with environments where `urllib3<=2.0.0` were installed. The log changes are incorporated into version 2.9.3 and greater.__
57+
**Note: this release was yanked from Pypi on 13 September 2023 due to compatibility issues with environments where `urllib3<=2.0.0` were installed. The log changes are incorporated into version 2.9.3 and greater.**
5458

5559
- Other: Add `examples/v3_retries_query_execute.py` (#199)
5660
- Other: suppress log message when `_enable_v3_retries` is not `True` (#199)
5761
- Other: make this connector backwards compatible with `urllib3>=1.0.0` (#197)
5862

5963
## 2.9.1 (2023-08-11)
6064

61-
__Note: this release was yanked from Pypi on 13 September 2023 due to compatibility issues with environments where `urllib3<=2.0.0` were installed.__
65+
**Note: this release was yanked from Pypi on 13 September 2023 due to compatibility issues with environments where `urllib3<=2.0.0` were installed.**
6266

6367
- Other: Explicitly pin urllib3 to ^2.0.0 (#191)
6468

@@ -111,6 +115,7 @@ __Note: this release was yanked from Pypi on 13 September 2023 due to compatibil
111115
- Other: Relax sqlalchemy required version as it was unecessarily strict.
112116

113117
## 2.5.0 (2023-04-14)
118+
114119
- Add support for External Auth providers
115120
- Fix: Python HTTP proxies were broken
116121
- Other: All Thrift requests that timeout during connection will be automatically retried
@@ -132,8 +137,8 @@ __Note: this release was yanked from Pypi on 13 September 2023 due to compatibil
132137

133138
## 2.2.2 (2023-01-03)
134139

135-
- Support custom oauth client id and redirect port
136-
- Fix: Add none check on _oauth_persistence in DatabricksOAuthProvider
140+
- Support custom oauth client id and redirect port
141+
- Fix: Add none check on \_oauth_persistence in DatabricksOAuthProvider
137142

138143
## 2.2.1 (2022-11-29)
139144

@@ -165,57 +170,71 @@ Huge thanks to @dbaxa for contributing this change!
165170

166171
- Add retry logic for `GetOperationStatus` requests that fail with an `OSError`
167172
- Reorganised code to use Poetry for dependency management.
173+
168174
## 2.0.2 (2022-05-04)
175+
169176
- Better exception handling in automatic connection close
170177

171178
## 2.0.1 (2022-04-21)
179+
172180
- Fixed Pandas dependency in setup.cfg to be >= 1.2.0
173181

174182
## 2.0.0 (2022-04-19)
183+
175184
- Initial stable release of V2
176-
- Added better support for complex types, so that in Databricks runtime 10.3+, Arrays, Maps and Structs will get
185+
- Added better support for complex types, so that in Databricks runtime 10.3+, Arrays, Maps and Structs will get
177186
deserialized as lists, lists of tuples and dicts, respectively.
178187
- Changed the name of the metadata arg to http_headers
179188

180189
## 2.0.b2 (2022-04-04)
190+
181191
- Change import of collections.Iterable to collections.abc.Iterable to make the library compatible with Python 3.10
182192
- Fixed bug with .tables method so that .tables works as expected with Unity-Catalog enabled endpoints
183193

184194
## 2.0.0b1 (2022-03-04)
195+
185196
- Fix packaging issue (dependencies were not being installed properly)
186197
- Fetching timestamp results will now return aware instead of naive timestamps
187198
- The client will now default to using simplified error messages
188199

189200
## 2.0.0b (2022-02-08)
201+
190202
- Initial beta release of V2. V2 is an internal re-write of large parts of the connector to use Databricks edge features. All public APIs from V1 remain.
191-
- Added Unity Catalog support (pass catalog and / or schema key word args to the .connect method to select initial schema and catalog)
203+
- Added Unity Catalog support (pass catalog and / or schema key word args to the .connect method to select initial schema and catalog)
192204

193205
---
194206

195207
**Note**: The code for versions prior to `v2.0.0b` is not contained in this repository. The below entries are included for reference only.
196208

197209
---
210+
198211
## 1.0.0 (2022-01-20)
212+
199213
- Add operations for retrieving metadata
200214
- Add the ability to access columns by name on result rows
201215
- Add the ability to provide configuration settings on connect
202216

203217
## 0.9.4 (2022-01-10)
218+
204219
- Improved logging and error messages.
205220

206221
## 0.9.3 (2021-12-08)
222+
207223
- Add retries for 429 and 503 HTTP responses.
208224

209225
## 0.9.2 (2021-12-02)
226+
210227
- (Bug fix) Increased Thrift requirement from 0.10.0 to 0.13.0 as 0.10.0 was in fact incompatible
211228
- (Bug fix) Fixed error message after query execution failed -SQLSTATE and Error message were misplaced
212229

213230
## 0.9.1 (2021-09-01)
231+
214232
- Public Preview release, Experimental tag removed
215233
- minor updates in internal build/packaging
216234
- no functional changes
217235

218236
## 0.9.0 (2021-08-04)
237+
219238
- initial (Experimental) release of pyhive-forked connector
220239
- Python DBAPI 2.0 (PEP-0249), thrift based
221240
- see docs for more info: https://docs.databricks.com/dev-tools/python-sql-connector.html

src/databricks/sql/auth/retry.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,8 +283,10 @@ def sleep_for_retry(self, response: BaseHTTPResponse) -> bool: # type: ignore
283283
"""
284284
retry_after = self.get_retry_after(response)
285285
if retry_after:
286-
self.check_proposed_wait(retry_after)
287-
time.sleep(retry_after)
286+
backoff = self.get_backoff_time()
287+
proposed_wait = max(backoff, retry_after)
288+
self.check_proposed_wait(proposed_wait)
289+
time.sleep(proposed_wait)
288290
return True
289291

290292
return False

tests/e2e/common/retry_test_mixins.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from contextlib import contextmanager
2+
import time
23
from typing import List
34
from unittest.mock import MagicMock, PropertyMock, patch
45

@@ -166,6 +167,33 @@ def test_retry_max_count_not_exceeded(self):
166167
pass
167168
assert mock_obj.return_value.getresponse.call_count == 6
168169

170+
def test_retry_exponential_backoff(self):
171+
"""GIVEN the retry policy is configured for reasonable exponential backoff
172+
WHEN the server sends nothing but 429 responses with retry-afters
173+
THEN the connector will use those retry-afters as a floor
174+
"""
175+
retry_policy = self._retry_policy.copy()
176+
retry_policy["_retry_delay_min"] = 1
177+
178+
time_start = time.time()
179+
with mocked_server_response(status=429, headers={"Retry-After": "3"}) as mock_obj:
180+
with pytest.raises(RequestError) as cm:
181+
with self.connection(extra_params=retry_policy) as conn:
182+
pass
183+
184+
duration = time.time() - time_start
185+
assert isinstance(cm.value.args[1], MaxRetryDurationError)
186+
187+
# With setting delay_min to 1, the expected retry delays should be:
188+
# 3, 3, 4
189+
# The first 2 retries are allowed, the 3rd retry puts the total duration over the limit
190+
# of 10 seconds
191+
assert mock_obj.return_value.getresponse.call_count == 3
192+
assert duration > 6
193+
194+
# Should be less than 7, but this is a safe margin for CI/CD slowness
195+
assert duration < 10
196+
169197
def test_retry_max_duration_not_exceeded(self):
170198
"""GIVEN the max attempt duration of 10 seconds
171199
WHEN the server sends a Retry-After header of 60 seconds

tests/unit/test_retry.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from os import error
2+
import time
3+
from unittest.mock import Mock, patch
4+
import pytest
5+
from requests import Request
6+
from urllib3 import HTTPResponse
7+
from databricks.sql.auth.retry import DatabricksRetryPolicy, RequestHistory
8+
9+
10+
class TestRetry:
11+
12+
@pytest.fixture()
13+
def retry_policy(self) -> DatabricksRetryPolicy:
14+
return DatabricksRetryPolicy(
15+
delay_min=1,
16+
delay_max=30,
17+
stop_after_attempts_count=3,
18+
stop_after_attempts_duration=900,
19+
delay_default=2,
20+
force_dangerous_codes=[],
21+
)
22+
23+
@pytest.fixture()
24+
def error_history(self) -> RequestHistory:
25+
return RequestHistory(
26+
method="POST", url=None, error=None, status=503, redirect_location=None
27+
)
28+
29+
@patch("time.sleep")
30+
def test_sleep__no_retry_after(self, t_mock, retry_policy, error_history):
31+
retry_policy._retry_start_time = time.time()
32+
retry_policy.history = [error_history, error_history]
33+
retry_policy.sleep(HTTPResponse(status=503))
34+
t_mock.assert_called_with(2)
35+
36+
@patch("time.sleep")
37+
def test_sleep__retry_after_is_binding(self, t_mock, retry_policy, error_history):
38+
retry_policy._retry_start_time = time.time()
39+
retry_policy.history = [error_history, error_history]
40+
retry_policy.sleep(HTTPResponse(status=503, headers={"Retry-After": "3"}))
41+
t_mock.assert_called_with(3)
42+
43+
@patch("time.sleep")
44+
def test_sleep__retry_after_present_but_not_binding(self, t_mock, retry_policy, error_history):
45+
retry_policy._retry_start_time = time.time()
46+
retry_policy.history = [error_history, error_history]
47+
retry_policy.sleep(HTTPResponse(status=503, headers={"Retry-After": "1"}))
48+
t_mock.assert_called_with(2)
49+
50+
@patch("time.sleep")
51+
def test_sleep__retry_after_surpassed(self, t_mock, retry_policy, error_history):
52+
retry_policy._retry_start_time = time.time()
53+
retry_policy.history = [error_history, error_history, error_history]
54+
retry_policy.sleep(HTTPResponse(status=503, headers={"Retry-After": "3"}))
55+
t_mock.assert_called_with(4)

0 commit comments

Comments
 (0)