Skip to content

Commit 2ca06f5

Browse files
mrutunjay-kinagiMrutunjay Kinagikevinjqliu
authored
feat(rest): add sigv4 retry configuration defaults (#3063)
<!-- Closes #3008 --> # Rationale for this change When REST catalog uses SigV4 signing, retries were not configured on the mounted adapter. This change adds explicit retry defaults for signed REST requests and makes retry count configurable. Changes: - Added `rest.sigv4.max-retries` catalog property. - Default retries set to 10 attempts. - Retries configured for idempotent methods (`GET`, `HEAD`, `OPTIONS`) and common throttling/transient codes (`429`, `500`, `502`, `503`, `504`). ## Are these changes tested? Yes. - Added `test_sigv4_adapter_default_retry_config`. - Added `test_sigv4_adapter_override_retry_config`. - Verified with targeted pytest run. ## Are there any user-facing changes? Yes. - New optional REST catalog property: `rest.sigv4.max-retries`. - Improved resilience for throttling/transient HTTP failures on SigV4-enabled REST catalogs. --------- Co-authored-by: Mrutunjay Kinagi <you@example.com> Co-authored-by: Kevin Liu <kevinjqliu@users.noreply.github.com> Co-authored-by: Kevin Liu <kevin.jq.liu@gmail.com>
1 parent ca807ae commit 2ca06f5

File tree

2 files changed

+77
-3
lines changed

2 files changed

+77
-3
lines changed

pyiceberg/catalog/rest/__init__.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,15 @@
5656
TableAlreadyExistsError,
5757
UnauthorizedError,
5858
)
59-
from pyiceberg.io import AWS_ACCESS_KEY_ID, AWS_REGION, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, FileIO, load_file_io
59+
from pyiceberg.io import (
60+
AWS_ACCESS_KEY_ID,
61+
AWS_PROFILE_NAME,
62+
AWS_REGION,
63+
AWS_SECRET_ACCESS_KEY,
64+
AWS_SESSION_TOKEN,
65+
FileIO,
66+
load_file_io,
67+
)
6068
from pyiceberg.partitioning import UNPARTITIONED_PARTITION_SPEC, PartitionSpec, assign_fresh_partition_spec_ids
6169
from pyiceberg.schema import Schema, assign_fresh_schema_ids
6270
from pyiceberg.table import (
@@ -78,7 +86,7 @@
7886
from pyiceberg.typedef import EMPTY_DICT, UTF8, IcebergBaseModel, Identifier, Properties
7987
from pyiceberg.types import transform_dict_value_to_str
8088
from pyiceberg.utils.deprecated import deprecation_message
81-
from pyiceberg.utils.properties import get_first_property_value, get_header_properties, property_as_bool
89+
from pyiceberg.utils.properties import get_first_property_value, get_header_properties, property_as_bool, property_as_int
8290

8391
if TYPE_CHECKING:
8492
import pyarrow as pa
@@ -229,6 +237,8 @@ class IdentifierKind(Enum):
229237
SIGV4 = "rest.sigv4-enabled"
230238
SIGV4_REGION = "rest.signing-region"
231239
SIGV4_SERVICE = "rest.signing-name"
240+
SIGV4_MAX_RETRIES = "rest.sigv4.max-retries"
241+
SIGV4_MAX_RETRIES_DEFAULT = 10
232242
EMPTY_BODY_SHA256: str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
233243
OAUTH2_SERVER_URI = "oauth2-server-uri"
234244
SNAPSHOT_LOADING_MODE = "snapshot-loading-mode"
@@ -710,9 +720,11 @@ def _init_sigv4(self, session: Session) -> None:
710720

711721
class SigV4Adapter(HTTPAdapter):
712722
def __init__(self, **properties: str):
713-
super().__init__()
714723
self._properties = properties
724+
max_retries = property_as_int(self._properties, SIGV4_MAX_RETRIES, SIGV4_MAX_RETRIES_DEFAULT)
725+
super().__init__(max_retries=max_retries)
715726
self._boto_session = boto3.Session(
727+
profile_name=get_first_property_value(self._properties, AWS_PROFILE_NAME),
716728
region_name=get_first_property_value(self._properties, AWS_REGION),
717729
botocore_session=self._properties.get(BOTOCORE_SESSION),
718730
aws_access_key_id=get_first_property_value(self._properties, AWS_ACCESS_KEY_ID),

tests/catalog/test_rest.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
DEFAULT_ENDPOINTS,
3434
EMPTY_BODY_SHA256,
3535
OAUTH2_SERVER_URI,
36+
SIGV4_MAX_RETRIES,
37+
SIGV4_MAX_RETRIES_DEFAULT,
3638
SNAPSHOT_LOADING_MODE,
3739
Capability,
3840
Endpoint,
@@ -529,6 +531,66 @@ def test_sigv4_sign_request_with_body(rest_mock: Mocker) -> None:
529531
assert prepared.headers.get("x-amz-content-sha256") != EMPTY_BODY_SHA256
530532

531533

534+
def test_sigv4_adapter_default_retry_config(rest_mock: Mocker) -> None:
535+
catalog = RestCatalog(
536+
"rest",
537+
**{
538+
"uri": TEST_URI,
539+
"token": TEST_TOKEN,
540+
"rest.sigv4-enabled": "true",
541+
"rest.signing-region": "us-west-2",
542+
"client.access-key-id": "id",
543+
"client.secret-access-key": "secret",
544+
},
545+
)
546+
547+
adapter = catalog._session.adapters[catalog.uri]
548+
assert isinstance(adapter, HTTPAdapter)
549+
assert adapter.max_retries.total == SIGV4_MAX_RETRIES_DEFAULT
550+
551+
552+
def test_sigv4_adapter_override_retry_config(rest_mock: Mocker) -> None:
553+
catalog = RestCatalog(
554+
"rest",
555+
**{
556+
"uri": TEST_URI,
557+
"token": TEST_TOKEN,
558+
"rest.sigv4-enabled": "true",
559+
"rest.signing-region": "us-west-2",
560+
"client.access-key-id": "id",
561+
"client.secret-access-key": "secret",
562+
SIGV4_MAX_RETRIES: "3",
563+
},
564+
)
565+
566+
adapter = catalog._session.adapters[catalog.uri]
567+
assert isinstance(adapter, HTTPAdapter)
568+
assert adapter.max_retries.total == 3
569+
570+
571+
def test_sigv4_uses_client_profile_name(rest_mock: Mocker) -> None:
572+
with mock.patch("boto3.Session") as mock_session:
573+
RestCatalog(
574+
"rest",
575+
**{
576+
"uri": TEST_URI,
577+
"token": TEST_TOKEN,
578+
"rest.sigv4-enabled": "true",
579+
"rest.signing-region": "us-west-2",
580+
"client.profile-name": "rest-profile",
581+
},
582+
)
583+
584+
mock_session.assert_called_with(
585+
profile_name="rest-profile",
586+
region_name=None,
587+
botocore_session=None,
588+
aws_access_key_id=None,
589+
aws_secret_access_key=None,
590+
aws_session_token=None,
591+
)
592+
593+
532594
def test_list_tables_404(rest_mock: Mocker) -> None:
533595
namespace = "examples"
534596
rest_mock.get(

0 commit comments

Comments
 (0)