Skip to content

Commit 2b48f7d

Browse files
committed
Address PR feedback: rename and clean up retry-after parameter
- Rename server_directed_only to respect_server_retry_after_header throughout for clarity - Store _respect_server_retry_after_header as instance variable in Thrift/SEA backends to match existing kwargs extraction pattern - Replace duplicate test fixture with _make_retry_policy(**overrides) helper for flexible policy construction in tests Signed-off-by: Shubham Dhal <shubham.dhal@databricks.com>
1 parent 0d583fa commit 2b48f7d

File tree

8 files changed

+70
-89
lines changed

8 files changed

+70
-89
lines changed

src/databricks/sql/auth/common.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def __init__(
4747
retry_stop_after_attempts_duration: Optional[float] = None,
4848
retry_delay_default: Optional[float] = None,
4949
retry_dangerous_codes: Optional[List[int]] = None,
50-
retry_server_directed_only: Optional[bool] = None,
50+
respect_server_retry_after_header: Optional[bool] = None,
5151
proxy_auth_method: Optional[str] = None,
5252
pool_connections: Optional[int] = None,
5353
pool_maxsize: Optional[int] = None,
@@ -81,7 +81,7 @@ def __init__(
8181
)
8282
self.retry_delay_default = retry_delay_default or 5.0
8383
self.retry_dangerous_codes = retry_dangerous_codes or []
84-
self.retry_server_directed_only = bool(retry_server_directed_only)
84+
self.respect_server_retry_after_header = bool(respect_server_retry_after_header)
8585
self.proxy_auth_method = proxy_auth_method
8686
self.pool_connections = pool_connections or 10
8787
self.pool_maxsize = pool_maxsize or 20

src/databricks/sql/auth/retry.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def __init__(
9494
stop_after_attempts_duration: float,
9595
delay_default: float,
9696
force_dangerous_codes: List[int],
97-
server_directed_only: bool = False,
97+
respect_server_retry_after_header: bool = False,
9898
urllib3_kwargs: dict = {},
9999
):
100100
# These values do not change from one command to the next
@@ -104,7 +104,7 @@ def __init__(
104104
self.stop_after_attempts_duration = stop_after_attempts_duration
105105
self._delay_default = delay_default
106106
self.force_dangerous_codes = force_dangerous_codes
107-
self.server_directed_only = server_directed_only
107+
self.respect_server_retry_after_header = respect_server_retry_after_header
108108

109109
# the urllib3 kwargs are a mix of configuration (some of which we override)
110110
# and counters like `total` or `connect` which may change between successive retries
@@ -204,7 +204,7 @@ def new(
204204
stop_after_attempts_duration=self.stop_after_attempts_duration,
205205
delay_default=self.delay_default,
206206
force_dangerous_codes=self.force_dangerous_codes,
207-
server_directed_only=self.server_directed_only,
207+
respect_server_retry_after_header=self.respect_server_retry_after_header,
208208
urllib3_kwargs={},
209209
)
210210

@@ -393,11 +393,11 @@ def should_retry(
393393
if not self._is_method_retryable(method):
394394
return False, "Only POST requests are retried"
395395

396-
# In server_directed_only mode, only retry when the server explicitly signals
397-
# it's safe via a Retry-After header. This prevents duplicate side effects for
398-
# non-idempotent operations.
399-
if self.server_directed_only and not has_retry_after:
400-
return (False, "server_directed_only mode: no Retry-After header present")
396+
# When respect_server_retry_after_header is enabled, only retry when the
397+
# server explicitly signals it's safe via a Retry-After header. This prevents
398+
# duplicate side effects for non-idempotent operations.
399+
if self.respect_server_retry_after_header and not has_retry_after:
400+
return (False, "respect_server_retry_after_header mode: no Retry-After header present")
401401

402402
# Request failed, was an ExecuteStatement and the command may have reached the server
403403
if (

src/databricks/sql/backend/sea/utils/http_client.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ def __init__(
9292
)
9393
self._retry_delay_default = kwargs.get("_retry_delay_default", 5.0)
9494
self.force_dangerous_codes = kwargs.get("_retry_dangerous_codes", [])
95+
self._respect_server_retry_after_header = kwargs.get(
96+
"_respect_server_retry_after_header", False
97+
)
9598

9699
# Connection pooling settings
97100
self.max_connections = kwargs.get("max_connections", 10)
@@ -116,7 +119,7 @@ def __init__(
116119
stop_after_attempts_duration=self._retry_stop_after_attempts_duration,
117120
delay_default=self._retry_delay_default,
118121
force_dangerous_codes=self.force_dangerous_codes,
119-
server_directed_only=kwargs.get("_retry_server_directed_only", False),
122+
respect_server_retry_after_header=self._respect_server_retry_after_header,
120123
urllib3_kwargs=urllib3_kwargs,
121124
)
122125
else:

src/databricks/sql/backend/thrift_backend.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,9 @@ def __init__(
191191
" This behaviour is deprecated and will be removed in a future release."
192192
)
193193
self.force_dangerous_codes = kwargs.get("_retry_dangerous_codes", [])
194+
self._respect_server_retry_after_header = kwargs.get(
195+
"_respect_server_retry_after_header", False
196+
)
194197

195198
additional_transport_args = {}
196199

@@ -217,7 +220,7 @@ def __init__(
217220
stop_after_attempts_duration=self._retry_stop_after_attempts_duration,
218221
delay_default=self._retry_delay_default,
219222
force_dangerous_codes=self.force_dangerous_codes,
220-
server_directed_only=kwargs.get("_retry_server_directed_only", False),
223+
respect_server_retry_after_header=self._respect_server_retry_after_header,
221224
urllib3_kwargs=urllib3_kwargs,
222225
)
223226

src/databricks/sql/common/unified_http_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def _setup_pool_managers(self):
135135
stop_after_attempts_duration=self.config.retry_stop_after_attempts_duration,
136136
delay_default=self.config.retry_delay_default,
137137
force_dangerous_codes=self.config.retry_dangerous_codes,
138-
server_directed_only=self.config.retry_server_directed_only,
138+
respect_server_retry_after_header=self.config.respect_server_retry_after_header,
139139
)
140140

141141
# Initialize the required attributes that DatabricksRetryPolicy expects

src/databricks/sql/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -977,7 +977,7 @@ def build_client_context(server_hostname: str, version: str, **kwargs):
977977
),
978978
retry_delay_default=kwargs.get("_retry_delay_default"),
979979
retry_dangerous_codes=kwargs.get("_retry_dangerous_codes"),
980-
retry_server_directed_only=kwargs.get("_retry_server_directed_only"),
980+
respect_server_retry_after_header=kwargs.get("_respect_server_retry_after_header"),
981981
proxy_auth_method=kwargs.get("_proxy_auth_method"),
982982
pool_connections=kwargs.get("_pool_connections"),
983983
pool_maxsize=kwargs.get("_pool_maxsize"),

tests/unit/test_retry.py

Lines changed: 49 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,21 @@
77

88

99
class TestRetry:
10-
@pytest.fixture()
11-
def retry_policy(self) -> DatabricksRetryPolicy:
12-
return DatabricksRetryPolicy(
10+
def _make_retry_policy(self, **overrides) -> DatabricksRetryPolicy:
11+
defaults = dict(
1312
delay_min=1,
1413
delay_max=30,
1514
stop_after_attempts_count=3,
1615
stop_after_attempts_duration=900,
1716
delay_default=2,
1817
force_dangerous_codes=[],
1918
)
19+
defaults.update(overrides)
20+
return DatabricksRetryPolicy(**defaults)
21+
22+
@pytest.fixture()
23+
def retry_policy(self) -> DatabricksRetryPolicy:
24+
return self._make_retry_policy()
2025

2126
@pytest.fixture()
2227
def error_history(self) -> RequestHistory:
@@ -84,112 +89,82 @@ def test_excessive_retry_attempts_error(self, t_mock, retry_policy):
8489
# Internally urllib3 calls the increment function generating a new instance for every retry
8590
retry_policy = retry_policy.increment()
8691

87-
@pytest.fixture()
88-
def server_directed_retry_policy(self) -> DatabricksRetryPolicy:
89-
return DatabricksRetryPolicy(
90-
delay_min=1,
91-
delay_max=30,
92-
stop_after_attempts_count=3,
93-
stop_after_attempts_duration=900,
94-
delay_default=2,
95-
force_dangerous_codes=[],
96-
server_directed_only=True,
97-
)
98-
99-
def test_server_directed_only__retries_with_retry_after(
100-
self, server_directed_retry_policy
101-
):
92+
def test_respect_server_retry_after__retries_with_retry_after(self):
10293
"""429 + Retry-After header → should retry"""
103-
server_directed_retry_policy._retry_start_time = time.time()
104-
server_directed_retry_policy.command_type = CommandType.OTHER
105-
should_retry, msg = server_directed_retry_policy.should_retry(
106-
"POST", 429, has_retry_after=True
107-
)
94+
policy = self._make_retry_policy(respect_server_retry_after_header=True)
95+
policy._retry_start_time = time.time()
96+
policy.command_type = CommandType.OTHER
97+
should_retry, msg = policy.should_retry("POST", 429, has_retry_after=True)
10898
assert should_retry is True
10999

110-
def test_server_directed_only__no_retry_without_retry_after(
111-
self, server_directed_retry_policy
112-
):
100+
def test_respect_server_retry_after__no_retry_without_retry_after(self):
113101
"""429 without Retry-After header → no retry"""
114-
server_directed_retry_policy._retry_start_time = time.time()
115-
server_directed_retry_policy.command_type = CommandType.OTHER
116-
should_retry, msg = server_directed_retry_policy.should_retry(
117-
"POST", 429, has_retry_after=False
118-
)
102+
policy = self._make_retry_policy(respect_server_retry_after_header=True)
103+
policy._retry_start_time = time.time()
104+
policy.command_type = CommandType.OTHER
105+
should_retry, msg = policy.should_retry("POST", 429, has_retry_after=False)
119106
assert should_retry is False
120-
assert "server_directed_only" in msg
107+
assert "respect_server_retry_after_header" in msg
121108

122-
def test_server_directed_only__no_retry_503_without_header(
123-
self, server_directed_retry_policy
124-
):
109+
def test_respect_server_retry_after__no_retry_503_without_header(self):
125110
"""503 without Retry-After header → no retry"""
126-
server_directed_retry_policy._retry_start_time = time.time()
127-
server_directed_retry_policy.command_type = CommandType.OTHER
128-
should_retry, msg = server_directed_retry_policy.should_retry(
129-
"POST", 503, has_retry_after=False
130-
)
111+
policy = self._make_retry_policy(respect_server_retry_after_header=True)
112+
policy._retry_start_time = time.time()
113+
policy.command_type = CommandType.OTHER
114+
should_retry, msg = policy.should_retry("POST", 503, has_retry_after=False)
131115
assert should_retry is False
132-
assert "server_directed_only" in msg
116+
assert "respect_server_retry_after_header" in msg
133117

134-
def test_server_directed_only__overrides_dangerous_codes(self):
135-
"""force_dangerous_codes=[500] + no Retry-After → no retry in server_directed_only mode"""
136-
policy = DatabricksRetryPolicy(
137-
delay_min=1,
138-
delay_max=30,
139-
stop_after_attempts_count=3,
140-
stop_after_attempts_duration=900,
141-
delay_default=2,
142-
force_dangerous_codes=[500],
143-
server_directed_only=True,
118+
def test_respect_server_retry_after__overrides_dangerous_codes(self):
119+
"""force_dangerous_codes=[500] + no Retry-After → no retry in respect_server_retry_after_header mode"""
120+
policy = self._make_retry_policy(
121+
force_dangerous_codes=[500], respect_server_retry_after_header=True
144122
)
145123
policy._retry_start_time = time.time()
146124
policy.command_type = CommandType.EXECUTE_STATEMENT
147125
should_retry, msg = policy.should_retry("POST", 500, has_retry_after=False)
148126
assert should_retry is False
149-
assert "server_directed_only" in msg
127+
assert "respect_server_retry_after_header" in msg
150128

151-
def test_server_directed_only__non_retryable_codes_unaffected(
152-
self, server_directed_retry_policy
153-
):
129+
def test_respect_server_retry_after__non_retryable_codes_unaffected(self):
154130
"""401/403/501 still don't retry even with Retry-After header"""
155-
server_directed_retry_policy._retry_start_time = time.time()
156-
server_directed_retry_policy.command_type = CommandType.OTHER
131+
policy = self._make_retry_policy(respect_server_retry_after_header=True)
132+
policy._retry_start_time = time.time()
133+
policy.command_type = CommandType.OTHER
157134
for code in [401, 403, 501]:
158-
should_retry, msg = server_directed_retry_policy.should_retry(
135+
should_retry, msg = policy.should_retry(
159136
"POST", code, has_retry_after=True
160137
)
161138
assert should_retry is False, f"Code {code} should never retry"
162139

163140
def test_default_mode_unchanged(self, retry_policy):
164-
"""server_directed_only=False preserves existing behavior — 429 retries without header"""
141+
"""respect_server_retry_after_header=False preserves existing behavior — 429 retries without header"""
165142
retry_policy._retry_start_time = time.time()
166143
retry_policy.command_type = CommandType.OTHER
167144
should_retry, msg = retry_policy.should_retry(
168145
"POST", 429, has_retry_after=False
169146
)
170147
assert should_retry is True
171148

172-
def test_server_directed_only__survives_new(self, server_directed_retry_policy):
149+
def test_respect_server_retry_after__survives_new(self):
173150
"""urllib3 calls .new() between retries to create a fresh policy instance.
174-
Verify that server_directed_only is carried over and still enforced."""
175-
server_directed_retry_policy._retry_start_time = time.time()
176-
server_directed_retry_policy.command_type = CommandType.OTHER
177-
new_policy = server_directed_retry_policy.new()
178-
assert new_policy.server_directed_only is True
151+
Verify that respect_server_retry_after_header is carried over and still enforced."""
152+
policy = self._make_retry_policy(respect_server_retry_after_header=True)
153+
policy._retry_start_time = time.time()
154+
policy.command_type = CommandType.OTHER
155+
new_policy = policy.new()
156+
assert new_policy.respect_server_retry_after_header is True
179157
# The new instance should still block retries without Retry-After
180158
should_retry, msg = new_policy.should_retry("POST", 429, has_retry_after=False)
181159
assert should_retry is False
182-
assert "server_directed_only" in msg
160+
assert "respect_server_retry_after_header" in msg
183161

184-
def test_server_directed_only__execute_statement_with_retry_after(
185-
self, server_directed_retry_policy
186-
):
162+
def test_respect_server_retry_after__execute_statement_with_retry_after(self):
187163
"""EXECUTE_STATEMENT + 429 + Retry-After header → retry"""
188-
server_directed_retry_policy._retry_start_time = time.time()
189-
server_directed_retry_policy.command_type = CommandType.EXECUTE_STATEMENT
190-
should_retry, msg = server_directed_retry_policy.should_retry(
191-
"POST", 429, has_retry_after=True
192-
)
164+
policy = self._make_retry_policy(respect_server_retry_after_header=True)
165+
policy._retry_start_time = time.time()
166+
policy.command_type = CommandType.EXECUTE_STATEMENT
167+
should_retry, msg = policy.should_retry("POST", 429, has_retry_after=True)
193168
assert should_retry is True
194169

195170
def test_404_does_not_retry_for_any_command_type(self, retry_policy):

tests/unit/test_unified_http_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def client_context(self):
3737
context.retry_stop_after_attempts_duration = 300.0
3838
context.retry_delay_default = 5.0
3939
context.retry_dangerous_codes = []
40-
context.retry_server_directed_only = False
40+
context.respect_server_retry_after_header = False
4141
context.proxy_auth_method = None
4242
context.pool_connections = 10
4343
context.pool_maxsize = 20

0 commit comments

Comments
 (0)