Skip to content

Commit 0f51dc5

Browse files
committed
REST: Apply rest.client timeouts to SigV4 requests
SigV4Adapter is mounted on the catalog URI and overrides the timeout adapter, so rest.client.*-timeout-ms was ignored when rest.sigv4-enabled is set.
1 parent 068aae5 commit 0f51dc5

3 files changed

Lines changed: 49 additions & 14 deletions

File tree

‎mkdocs/docs/configuration.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,8 @@ Legacy OAuth2 Properties will be removed in PyIceberg 1.0 in place of pluggable
478478
| rest.signing-region | us-east-1 | The region to use when SigV4 signing a request |
479479
| rest.signing-name | execute-api | The service signing name to use when SigV4 signing a request |
480480

481+
The `rest.client.connection-timeout-ms` and `rest.client.socket-timeout-ms` timeouts also apply to SigV4-signed requests. SigV4 retries are controlled by `rest.sigv4.max-retries`, not `rest.client.max-retries`.
482+
481483
##### Pluggable Authentication via AuthManager
482484

483485
The RESTCatalog supports pluggable authentication via the `auth` configuration block. This allows you to specify which how the access token will be fetched and managed for use with the HTTP requests to the RESTCatalog server. The authentication method is selected by setting the `auth.type` property, and additional configuration can be provided as needed for each method.

‎pyiceberg/catalog/rest/__init__.py‎

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -499,11 +499,10 @@ def send(
499499
return super().send(request, stream=stream, timeout=timeout, verify=verify, cert=cert, proxies=proxies)
500500

501501

502-
def _create_connection_adapter(properties: Properties) -> _RetryTimeoutHTTPAdapter | None:
503-
"""Build a connection adapter from the optional `rest.client.*` properties.
502+
def _connection_timeout(properties: Properties) -> float | None:
503+
"""Compute the default request timeout in seconds from the optional `rest.client.*` timeout properties.
504504
505-
Returns None when no connection properties are supplied, leaving the default
506-
Session behavior unchanged. Raises ValueError on invalid input.
505+
Raises ValueError on invalid input.
507506
"""
508507
connection_timeout_ms = property_as_int(properties, REST_CLIENT_CONNECTION_TIMEOUT_MS)
509508
if connection_timeout_ms is not None and connection_timeout_ms <= 0:
@@ -513,6 +512,19 @@ def _create_connection_adapter(properties: Properties) -> _RetryTimeoutHTTPAdapt
513512
if socket_timeout_ms is not None and socket_timeout_ms <= 0:
514513
raise ValueError(f"`{REST_CLIENT_SOCKET_TIMEOUT_MS}` must be a positive number, got: {socket_timeout_ms}")
515514

515+
# requests uses a single timeout and cannot split connect vs socket, so follow the Java client
516+
# and sum the two (milliseconds), flooring to whole seconds.
517+
if connection_timeout_ms is None and socket_timeout_ms is None:
518+
return None
519+
return ((connection_timeout_ms or 0) + (socket_timeout_ms or 0)) // 1000
520+
521+
522+
def _create_connection_adapter(properties: Properties) -> _RetryTimeoutHTTPAdapter | None:
523+
"""Build a connection adapter from the optional `rest.client.*` properties.
524+
525+
Returns None when no connection properties are supplied, leaving the default
526+
Session behavior unchanged. Raises ValueError on invalid input.
527+
"""
516528
retries = property_as_int(properties, REST_CLIENT_MAX_RETRIES)
517529
if retries is not None and retries < 0:
518530
raise ValueError(f"`{REST_CLIENT_MAX_RETRIES}` must be non-negative, got: {retries}")
@@ -521,14 +533,10 @@ def _create_connection_adapter(properties: Properties) -> _RetryTimeoutHTTPAdapt
521533
if backoff_factor is not None and backoff_factor < 0:
522534
raise ValueError(f"`{REST_CLIENT_RETRY_BACKOFF_FACTOR}` must be non-negative, got: {backoff_factor}")
523535

524-
if all(value is None for value in (connection_timeout_ms, socket_timeout_ms, retries, backoff_factor)):
525-
return None
536+
timeout = _connection_timeout(properties)
526537

527-
# requests uses a single timeout and cannot split connect vs socket, so follow the Java client
528-
# and sum the two (milliseconds), flooring to whole seconds.
529-
timeout: float | None = None
530-
if connection_timeout_ms is not None or socket_timeout_ms is not None:
531-
timeout = ((connection_timeout_ms or 0) + (socket_timeout_ms or 0)) // 1000
538+
if all(value is None for value in (timeout, retries, backoff_factor)):
539+
return None
532540

533541
return _RetryTimeoutHTTPAdapter(
534542
timeout=timeout,
@@ -575,7 +583,8 @@ def _create_session(self) -> Session:
575583
session = Session()
576584

577585
# Mount the retry/timeout adapter when `connection.*` properties are set.
578-
# SigV4's adapter mounted below at `self.uri` is a longer prefix and still wins for that host.
586+
# SigV4's adapter mounted below at `self.uri` is a longer prefix and still wins for that host,
587+
# so it applies the same timeout itself.
579588
if (connection_adapter := _create_connection_adapter(self.properties)) is not None:
580589
session.mount("http://", connection_adapter)
581590
session.mount("https://", connection_adapter)
@@ -1098,11 +1107,14 @@ def _init_sigv4(self, session: Session) -> None:
10981107
from botocore.auth import SigV4Auth
10991108
from botocore.awsrequest import AWSRequest
11001109

1101-
class SigV4Adapter(HTTPAdapter):
1110+
class SigV4Adapter(_RetryTimeoutHTTPAdapter):
11021111
def __init__(self, **properties: str):
11031112
self._properties = properties
11041113
max_retries = property_as_int(self._properties, SIGV4_MAX_RETRIES, SIGV4_MAX_RETRIES_DEFAULT)
1105-
super().__init__(max_retries=max_retries)
1114+
super().__init__(
1115+
timeout=_connection_timeout(self._properties),
1116+
max_retries=SIGV4_MAX_RETRIES_DEFAULT if max_retries is None else max_retries,
1117+
)
11061118
self._boto_session = boto3.Session(
11071119
profile_name=get_first_property_value(self._properties, AWS_PROFILE_NAME),
11081120
region_name=get_first_property_value(self._properties, AWS_REGION),

‎tests/catalog/test_rest.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,27 @@ def test_sigv4_adapter_override_retry_config(rest_mock: Mocker) -> None:
695695
assert adapter.max_retries.total == 3
696696

697697

698+
def test_sigv4_adapter_applies_client_timeout(rest_mock: Mocker) -> None:
699+
catalog = RestCatalog(
700+
"rest",
701+
**{
702+
"uri": TEST_URI,
703+
"token": TEST_TOKEN,
704+
"rest.sigv4-enabled": "true",
705+
"rest.signing-region": "us-west-2",
706+
"client.access-key-id": "id",
707+
"client.secret-access-key": "secret",
708+
"rest.client.connection-timeout-ms": "1000",
709+
"rest.client.socket-timeout-ms": "2000",
710+
},
711+
)
712+
713+
adapter = catalog._session.adapters[catalog.uri]
714+
assert isinstance(adapter, _RetryTimeoutHTTPAdapter)
715+
assert adapter._timeout == 3
716+
assert adapter.max_retries.total == SIGV4_MAX_RETRIES_DEFAULT
717+
718+
698719
def test_sigv4_uses_client_profile_name(rest_mock: Mocker) -> None:
699720
with mock.patch("boto3.Session") as mock_session:
700721
RestCatalog(

0 commit comments

Comments
 (0)