Skip to content

Commit c717cac

Browse files
committed
Apply rest.client timeouts to the SigV4 adapter
The SigV4 adapter is mounted on the catalog URI and wins over the timeout adapter, so rest.client.connection-timeout-ms and socket-timeout-ms were ignored with SigV4 (#4076).
1 parent 0cc389b commit c717cac

2 files changed

Lines changed: 38 additions & 11 deletions

File tree

‎pyiceberg/catalog/rest/__init__.py‎

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

501501

502+
def _connection_timeout(properties: Properties) -> float | None:
503+
"""Return the request timeout in seconds from the `rest.client.*` timeout properties, or None when unset."""
504+
connection_timeout_ms = property_as_int(properties, REST_CLIENT_CONNECTION_TIMEOUT_MS)
505+
socket_timeout_ms = property_as_int(properties, REST_CLIENT_SOCKET_TIMEOUT_MS)
506+
if connection_timeout_ms is None and socket_timeout_ms is None:
507+
return None
508+
# requests uses a single timeout and cannot split connect vs socket, so follow the Java client
509+
# and sum the two (milliseconds), flooring to whole seconds.
510+
return ((connection_timeout_ms or 0) + (socket_timeout_ms or 0)) // 1000
511+
512+
502513
def _create_connection_adapter(properties: Properties) -> _RetryTimeoutHTTPAdapter | None:
503514
"""Build a connection adapter from the optional `rest.client.*` properties.
504515
@@ -524,14 +535,8 @@ def _create_connection_adapter(properties: Properties) -> _RetryTimeoutHTTPAdapt
524535
if all(value is None for value in (connection_timeout_ms, socket_timeout_ms, retries, backoff_factor)):
525536
return None
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
532-
533538
return _RetryTimeoutHTTPAdapter(
534-
timeout=timeout,
539+
timeout=_connection_timeout(properties),
535540
max_retries=Retry(
536541
# `retries` and `backoff_factor` fall back to a no-op Retry when unset, so a user can
537542
# configure only one without having to specify the rest of the policy.
@@ -575,7 +580,7 @@ def _create_session(self) -> Session:
575580
session = Session()
576581

577582
# 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.
583+
# SigV4's adapter mounted below at `self.uri` is a longer prefix and wins for that host; it applies the same timeout.
579584
if (connection_adapter := _create_connection_adapter(self.properties)) is not None:
580585
session.mount("http://", connection_adapter)
581586
session.mount("https://", connection_adapter)
@@ -1098,11 +1103,11 @@ def _init_sigv4(self, session: Session) -> None:
10981103
from botocore.auth import SigV4Auth
10991104
from botocore.awsrequest import AWSRequest
11001105

1101-
class SigV4Adapter(HTTPAdapter):
1106+
class SigV4Adapter(_RetryTimeoutHTTPAdapter):
11021107
def __init__(self, **properties: str):
11031108
self._properties = properties
1104-
max_retries = property_as_int(self._properties, SIGV4_MAX_RETRIES, SIGV4_MAX_RETRIES_DEFAULT)
1105-
super().__init__(max_retries=max_retries)
1109+
max_retries: int = property_as_int(self._properties, SIGV4_MAX_RETRIES, SIGV4_MAX_RETRIES_DEFAULT) # type: ignore # The default is set with non-None value.
1110+
super().__init__(timeout=_connection_timeout(self._properties), max_retries=max_retries)
11061111
self._boto_session = boto3.Session(
11071112
profile_name=get_first_property_value(self._properties, AWS_PROFILE_NAME),
11081113
region_name=get_first_property_value(self._properties, AWS_REGION),

‎tests/catalog/test_rest.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,28 @@ 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_connection_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: "5000",
709+
REST_CLIENT_SOCKET_TIMEOUT_MS: "60000",
710+
},
711+
)
712+
713+
# The SigV4 adapter is mounted on the catalog URI, so it handles catalog requests instead of the `https://` adapter.
714+
adapter = catalog._session.get_adapter(catalog.uri)
715+
assert isinstance(adapter, _RetryTimeoutHTTPAdapter)
716+
assert adapter._timeout == 65 # (5000 + 60000) ms floored to whole seconds
717+
assert adapter.max_retries.total == SIGV4_MAX_RETRIES_DEFAULT
718+
719+
698720
def test_sigv4_uses_client_profile_name(rest_mock: Mocker) -> None:
699721
with mock.patch("boto3.Session") as mock_session:
700722
RestCatalog(

0 commit comments

Comments
 (0)