|
24 | 24 | from copy import copy |
25 | 25 | from dataclasses import dataclass |
26 | 26 | from enum import Enum |
27 | | -from functools import cached_property, singledispatch |
| 27 | +from functools import cached_property, partial, singledispatch |
28 | 28 | from itertools import chain |
29 | 29 | from typing import ( |
30 | 30 | TYPE_CHECKING, |
|
43 | 43 |
|
44 | 44 | from pydantic import Field, SerializeAsAny |
45 | 45 | from sortedcontainers import SortedList |
| 46 | +from tenacity import RetryError, Retrying, retry_if_exception_type, stop_after_attempt, stop_after_delay, wait_exponential |
46 | 47 | from typing_extensions import Annotated |
47 | 48 |
|
48 | 49 | from pyiceberg.exceptions import CommitFailedException, ResolveError, ValidationError |
@@ -791,6 +792,76 @@ class CommitTableResponse(IcebergBaseModel): |
791 | 792 | metadata_location: str = Field(alias="metadata-location") |
792 | 793 |
|
793 | 794 |
|
| 795 | +class TableCommitRetry: |
| 796 | + """Decorator for building the table commit retry controller.""" |
| 797 | + |
| 798 | + num_retries = "commit.retry.num-retries" |
| 799 | + num_retries_default: int = 4 |
| 800 | + min_wait_ms = "commit.retry.min-wait-ms" |
| 801 | + min_wait_ms_default: int = 100 |
| 802 | + max_wait_ms = "commit.retry.max-wait-ms" |
| 803 | + max_wait_ms_default: int = 60000 # 1 min |
| 804 | + total_timeout_ms = "commit.retry.total-timeout-ms" |
| 805 | + total_timeout_ms_default: int = 1800000 # 30 mins |
| 806 | + |
| 807 | + def __init__(self, func: Callable[..., Any], properties_attribute: str = "properties") -> None: |
| 808 | + self.properties_attr: str = properties_attribute |
| 809 | + self.func: Callable[..., Any] = func |
| 810 | + self.loaded_properties: Properties = {} |
| 811 | + |
| 812 | + def __get__(self, instance: Any, owner: Any) -> Callable[..., Any]: |
| 813 | + """Return the __call__ method with the instance caller.""" |
| 814 | + return partial(self.__call__, instance) |
| 815 | + |
| 816 | + def __call__(self, instance: Any, *args: Any, **kwargs: Any) -> Any: |
| 817 | + """Run function with the retrying controller on the caller instance.""" |
| 818 | + self.loaded_properties = getattr(instance, self.properties_attr) |
| 819 | + try: |
| 820 | + for attempt in self.build_retry_controller(): |
| 821 | + with attempt: |
| 822 | + result = self.func(instance, *args, **kwargs) |
| 823 | + except RetryError as err: |
| 824 | + raise Exception from err.reraise() |
| 825 | + else: |
| 826 | + return result |
| 827 | + |
| 828 | + @property |
| 829 | + def table_properties(self) -> Properties: |
| 830 | + """Get the table properties from the instance that is calling this decorator.""" |
| 831 | + return self.loaded_properties |
| 832 | + |
| 833 | + def build_retry_controller(self) -> Retrying: |
| 834 | + """Build the retry controller.""" |
| 835 | + return Retrying( |
| 836 | + stop=( |
| 837 | + stop_after_attempt(self.get_config(self.num_retries, self.num_retries_default)) |
| 838 | + | stop_after_delay( |
| 839 | + datetime.timedelta(milliseconds=self.get_config(self.total_timeout_ms, self.total_timeout_ms_default)) |
| 840 | + ) |
| 841 | + ), |
| 842 | + wait=wait_exponential(min=self.get_config(self.min_wait_ms, self.min_wait_ms_default) / 1000.0), |
| 843 | + retry=retry_if_exception_type(CommitFailedException), |
| 844 | + ) |
| 845 | + |
| 846 | + def get_config(self, config: str, default: int) -> int: |
| 847 | + """Get config out of the properties.""" |
| 848 | + return self.to_int(self.table_properties.get(config, ""), default) |
| 849 | + |
| 850 | + @staticmethod |
| 851 | + def to_int(v: str, default: int) -> int: |
| 852 | + """Convert str value to int, otherwise return a default.""" |
| 853 | + try: |
| 854 | + return int(v) |
| 855 | + except (ValueError, TypeError): |
| 856 | + pass |
| 857 | + return default |
| 858 | + |
| 859 | + |
| 860 | +def table_commit_retry(properties_attribute: str) -> Callable[..., TableCommitRetry]: |
| 861 | + """Decorate TableCommitRetry to capture the `properties_attribute`.""" |
| 862 | + return partial(TableCommitRetry, properties_attribute=properties_attribute) |
| 863 | + |
| 864 | + |
794 | 865 | class Table: |
795 | 866 | identifier: Identifier = Field() |
796 | 867 | metadata: TableMetadata |
@@ -994,6 +1065,7 @@ def refs(self) -> Dict[str, SnapshotRef]: |
994 | 1065 | """Return the snapshot references in the table.""" |
995 | 1066 | return self.metadata.refs |
996 | 1067 |
|
| 1068 | + @table_commit_retry("properties") |
997 | 1069 | def _do_commit(self, updates: Tuple[TableUpdate, ...], requirements: Tuple[TableRequirement, ...]) -> None: |
998 | 1070 | response = self.catalog._commit_table( # pylint: disable=W0212 |
999 | 1071 | CommitTableRequest( |
|
0 commit comments