From ea9f22560f33ab98f1b45476f670b655ab07a814 Mon Sep 17 00:00:00 2001 From: Natan Tolparov Date: Tue, 25 Mar 2025 14:04:57 +0300 Subject: [PATCH 1/3] feat(retries): added throttling mode --- README.md | 16 ++++++++-------- tests/test_retry_policy.py | 9 +++++---- uv.lock | 2 +- yandexcloud/__init__.py | 2 +- yandexcloud/_retry_policy.py | 15 +++++++++++++-- 5 files changed, 28 insertions(+), 16 deletions(-) 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..030a75bd 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() @@ -65,7 +65,7 @@ def test_default_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..0a74d8aa 100644 --- a/yandexcloud/_retry_policy.py +++ b/yandexcloud/_retry_policy.py @@ -1,14 +1,21 @@ import json +from enum import Enum from typing import Tuple import grpc +class ThrottlingMode(Enum): + PERSISTENT = "persistent" + TEMPORARY = "temporary" + + 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 +28,14 @@ def __init__( "backoffMultiplier": 2, "retryableStatusCodes": [status.name for status in status_codes], }, + "waitForReady": True, } ], - "retryThrottling": {"maxTokens": 100, "tokenRatio": 0.1}, - "waitForReady": True, + "retryThrottling": ( + {"maxTokens": 100, "tokenRatio": 0.1} + if throttling_mode == ThrottlingMode.PERSISTENT + else {"maxTokens": 6, "tokenRatio": 0.1} + ), } def to_json(self) -> str: From ca69282a4d62617f88d8f9960349d3678b7a021a Mon Sep 17 00:00:00 2001 From: Natan Tolparov Date: Tue, 25 Mar 2025 19:30:56 +0300 Subject: [PATCH 2/3] feat(retries): after review --- tests/test_retry_policy.py | 2 +- yandexcloud/_retry_policy.py | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/test_retry_policy.py b/tests/test_retry_policy.py index 030a75bd..91695625 100644 --- a/tests/test_retry_policy.py +++ b/tests/test_retry_policy.py @@ -61,7 +61,7 @@ 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( diff --git a/yandexcloud/_retry_policy.py b/yandexcloud/_retry_policy.py index 0a74d8aa..1d710ac8 100644 --- a/yandexcloud/_retry_policy.py +++ b/yandexcloud/_retry_policy.py @@ -1,6 +1,6 @@ import json from enum import Enum -from typing import Tuple +from typing import Any, Dict, Tuple import grpc @@ -10,6 +10,13 @@ class ThrottlingMode(Enum): 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, @@ -31,11 +38,7 @@ def __init__( "waitForReady": True, } ], - "retryThrottling": ( - {"maxTokens": 100, "tokenRatio": 0.1} - if throttling_mode == ThrottlingMode.PERSISTENT - else {"maxTokens": 6, "tokenRatio": 0.1} - ), + "retryThrottling": get_throttling_policy(throttling_mode), } def to_json(self) -> str: From 50e718831b33be206eee6efad58056fb6b4141f0 Mon Sep 17 00:00:00 2001 From: Natan Tolparov Date: Tue, 25 Mar 2025 19:34:11 +0300 Subject: [PATCH 3/3] feat(retries): after review more --- yandexcloud/_retry_policy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yandexcloud/_retry_policy.py b/yandexcloud/_retry_policy.py index 1d710ac8..d0c5b1c6 100644 --- a/yandexcloud/_retry_policy.py +++ b/yandexcloud/_retry_policy.py @@ -5,7 +5,7 @@ import grpc -class ThrottlingMode(Enum): +class ThrottlingMode(str, Enum): PERSISTENT = "persistent" TEMPORARY = "temporary"