diff --git a/README.md b/README.md index d8bd8605..a77a4f68 100644 --- a/README.md +++ b/README.md @@ -157,22 +157,22 @@ set_up_yc_api_endpoint(kz_region_endpoint) ``` ### Retries -If you want to retry SDK requests, build SDK with `retry_policy` field using `RetryPolicy` class: +SDK provide built-in retry policy, that supports [exponential backoff and jitter](https://aws.amazon.com/ru/blogs/architecture/exponential-backoff-and-jitter/), and also [retry budget](https://github.com/grpc/proposal/blob/master/A6-client-retries.md#throttling-retry-attempts-and-hedged-rpcs). It's necessary to avoid retry amplification. ```python import grpc from yandexcloud import SDK, RetryPolicy -sdk = SDK(retry_policy=RetryPolicy(max_attempts=2, status_codes=(grpc.StatusCode.UNAVAILABLE,))) +sdk = SDK(retry_policy=RetryPolicy()) ``` -It's **strongly recommended** to use default settings of RetryPolicy to avoid retry amplification: -```python -import grpc -from yandexcloud import SDK, RetryPolicy +SDK provide different modes for retry throttling policy: + +* `persistent` is suitable when you use SDK in any long-lived application, when SDK instance will live long enough for manage budget; +* `temporary` is suitable when you use SDK in any short-lived application, e.g. scripts or CI/CD. + +By default, SDK will use temporary mode, but you can change it through `throttling_mode` argument. -sdk = SDK(retry_policy=RetryPolicy()) -``` ## Contributing ### Dependencies diff --git a/tests/test_retry_policy.py b/tests/test_retry_policy.py index 49fefa0c..91695625 100644 --- a/tests/test_retry_policy.py +++ b/tests/test_retry_policy.py @@ -10,7 +10,7 @@ NetworkServiceStub, add_NetworkServiceServicer_to_server, ) -from yandexcloud import SDK, RetryPolicy +from yandexcloud import SDK, RetryPolicy, ThrottlingMode from yandexcloud._channels import Channels INSECURE_SERVICE_PORT = "50051" @@ -27,7 +27,7 @@ def side_effect_unavailable(_, context): class VPCServiceMock: def __init__(self, fn): - self.Get = Mock(return_value=Network(id="12342314")) + self.Get = Mock(side_effect=fn) self.Create = Mock() self.Update = Mock() self.Delete = Mock() @@ -61,11 +61,11 @@ def grpc_server(side_effect): return server, service -def test_default_retries(mock_channel): +def test_persistent_retries(mock_channel): server, service = grpc_server(side_effect_unavailable) sdk = SDK( - retry_policy=RetryPolicy(), + retry_policy=RetryPolicy(throttling_mode=ThrottlingMode.PERSISTENT), endpoint=f"localhost:{INSECURE_SERVICE_PORT}", endpoints={ "vpc": SERVICE_ADDR + ":" + INSECURE_SERVICE_PORT, @@ -98,7 +98,8 @@ def test_custom_retries(mock_channel): request = GetNetworkRequest(network_id="asdf") network_client.Get(request) except grpc.RpcError: - assert service.Get.call_count == 4 + # because of temporary throttling mode by default + assert service.Get.call_count == 3 server.stop(0) diff --git a/uv.lock b/uv.lock index b45a51eb..901ab43d 100644 --- a/uv.lock +++ b/uv.lock @@ -1465,7 +1465,7 @@ wheels = [ [[package]] name = "yandexcloud" -version = "0.333.0" +version = "0.336.0" source = { editable = "." } dependencies = [ { name = "cryptography" }, diff --git a/yandexcloud/__init__.py b/yandexcloud/__init__.py index 08bb37ee..e7b97d6a 100644 --- a/yandexcloud/__init__.py +++ b/yandexcloud/__init__.py @@ -8,7 +8,7 @@ default_backoff, ) from yandexcloud._retry_interceptor import RetryInterceptor -from yandexcloud._retry_policy import RetryPolicy +from yandexcloud._retry_policy import RetryPolicy, ThrottlingMode from yandexcloud._sdk import SDK __version__ = "0.336.0" diff --git a/yandexcloud/_retry_policy.py b/yandexcloud/_retry_policy.py index b04b67ca..d0c5b1c6 100644 --- a/yandexcloud/_retry_policy.py +++ b/yandexcloud/_retry_policy.py @@ -1,14 +1,28 @@ import json -from typing import Tuple +from enum import Enum +from typing import Any, Dict, Tuple import grpc +class ThrottlingMode(str, Enum): + PERSISTENT = "persistent" + TEMPORARY = "temporary" + + +def get_throttling_policy(throttling_mode: ThrottlingMode) -> Dict[str, Any]: + if throttling_mode == ThrottlingMode.PERSISTENT: + return {"maxTokens": 100, "tokenRatio": 0.1} + + return {"maxTokens": 6, "tokenRatio": 0.1} + + class RetryPolicy: def __init__( self, max_attempts: int = 4, status_codes: Tuple["grpc.StatusCode"] = (grpc.StatusCode.UNAVAILABLE,), + throttling_mode: ThrottlingMode = ThrottlingMode.TEMPORARY, ): self.__policy = { "methodConfig": [ @@ -21,10 +35,10 @@ def __init__( "backoffMultiplier": 2, "retryableStatusCodes": [status.name for status in status_codes], }, + "waitForReady": True, } ], - "retryThrottling": {"maxTokens": 100, "tokenRatio": 0.1}, - "waitForReady": True, + "retryThrottling": get_throttling_policy(throttling_mode), } def to_json(self) -> str: