Skip to content

Commit 8f9d84b

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 2a85688 commit 8f9d84b

File tree

7 files changed

+69
-88
lines changed

7 files changed

+69
-88
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,
@@ -80,7 +80,7 @@ def __init__(
8080
)
8181
self.retry_delay_default = retry_delay_default or 5.0
8282
self.retry_dangerous_codes = retry_dangerous_codes or []
83-
self.retry_server_directed_only = bool(retry_server_directed_only)
83+
self.respect_server_retry_after_header = bool(respect_server_retry_after_header)
8484
self.proxy_auth_method = proxy_auth_method
8585
self.pool_connections = pool_connections or 10
8686
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

@@ -386,11 +386,11 @@ def should_retry(
386386
if not self._is_method_retryable(method):
387387
return False, "Only POST requests are retried"
388388

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

395395
# Request failed with 404 and was a GetOperationStatus. This is not recoverable. Don't retry.
396396
if status_code == 404 and self.command_type == CommandType.GET_OPERATION_STATUS:

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ def __init__(
9090
)
9191
self._retry_delay_default = kwargs.get("_retry_delay_default", 5.0)
9292
self.force_dangerous_codes = kwargs.get("_retry_dangerous_codes", [])
93+
self._respect_server_retry_after_header = kwargs.get(
94+
"_respect_server_retry_after_header", False
95+
)
9396

9497
# Connection pooling settings
9598
self.max_connections = kwargs.get("max_connections", 10)
@@ -114,7 +117,7 @@ def __init__(
114117
stop_after_attempts_duration=self._retry_stop_after_attempts_duration,
115118
delay_default=self._retry_delay_default,
116119
force_dangerous_codes=self.force_dangerous_codes,
117-
server_directed_only=kwargs.get("_retry_server_directed_only", False),
120+
respect_server_retry_after_header=self._respect_server_retry_after_header,
118121
urllib3_kwargs=urllib3_kwargs,
119122
)
120123
else:

src/databricks/sql/backend/thrift_backend.py

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

193196
additional_transport_args = {}
194197

@@ -215,7 +218,7 @@ def __init__(
215218
stop_after_attempts_duration=self._retry_stop_after_attempts_duration,
216219
delay_default=self._retry_delay_default,
217220
force_dangerous_codes=self.force_dangerous_codes,
218-
server_directed_only=kwargs.get("_retry_server_directed_only", False),
221+
respect_server_retry_after_header=self._respect_server_retry_after_header,
219222
urllib3_kwargs=urllib3_kwargs,
220223
)
221224

src/databricks/sql/common/unified_http_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def _setup_pool_managers(self):
9999
stop_after_attempts_duration=self.config.retry_stop_after_attempts_duration,
100100
delay_default=self.config.retry_delay_default,
101101
force_dangerous_codes=self.config.retry_dangerous_codes,
102-
server_directed_only=self.config.retry_server_directed_only,
102+
respect_server_retry_after_header=self.config.respect_server_retry_after_header,
103103
)
104104

105105
# 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
@@ -919,7 +919,7 @@ def build_client_context(server_hostname: str, version: str, **kwargs):
919919
),
920920
retry_delay_default=kwargs.get("_retry_delay_default"),
921921
retry_dangerous_codes=kwargs.get("_retry_dangerous_codes"),
922-
retry_server_directed_only=kwargs.get("_retry_server_directed_only"),
922+
respect_server_retry_after_header=kwargs.get("_respect_server_retry_after_header"),
923923
proxy_auth_method=kwargs.get("_proxy_auth_method"),
924924
pool_connections=kwargs.get("_pool_connections"),
925925
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):

0 commit comments

Comments
 (0)