Skip to content

Commit

Permalink
feat(client): adjust retry behavior to be exponential backoff (#247)
Browse files Browse the repository at this point in the history
  • Loading branch information
stainless-bot committed Oct 24, 2023
1 parent a192dee commit 7dcfdd8
Show file tree
Hide file tree
Showing 2 changed files with 31 additions and 27 deletions.
10 changes: 5 additions & 5 deletions src/modern_treasury/_base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ class BasePage(GenericModel, Generic[ModelT]):
Methods:
has_next_page(): Check if there is another page available
next_page_info(): Get the necesary information to make a request for the next page
next_page_info(): Get the necessary information to make a request for the next page
"""

_options: FinalRequestOptions = PrivateAttr()
Expand Down Expand Up @@ -691,15 +691,15 @@ def _calculate_retry_timeout(
return retry_after

initial_retry_delay = 0.5
max_retry_delay = 2.0
max_retry_delay = 8.0
nb_retries = max_retries - remaining_retries

# Apply exponential backoff, but not more than the max.
sleep_seconds = min(initial_retry_delay * pow(nb_retries - 1, 2), max_retry_delay)
sleep_seconds = min(initial_retry_delay * pow(2.0, nb_retries), max_retry_delay)

# Apply some jitter, plus-or-minus half a second.
jitter = random() - 0.5
timeout = sleep_seconds + jitter
jitter = 1 - 0.25 * random()
timeout = sleep_seconds * jitter
return timeout if timeout >= 0 else 0

def _should_retry(self, response: httpx.Response) -> bool:
Expand Down
48 changes: 26 additions & 22 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -728,18 +728,20 @@ class Model(BaseModel):
"remaining_retries,retry_after,timeout",
[
[3, "20", 20],
[3, "0", 2],
[3, "-10", 2],
[3, "0", 0.5],
[3, "-10", 0.5],
[3, "60", 60],
[3, "61", 2],
[3, "61", 0.5],
[3, "Fri, 29 Sep 2023 16:26:57 GMT", 20],
[3, "Fri, 29 Sep 2023 16:26:37 GMT", 2],
[3, "Fri, 29 Sep 2023 16:26:27 GMT", 2],
[3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5],
[3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5],
[3, "Fri, 29 Sep 2023 16:27:37 GMT", 60],
[3, "Fri, 29 Sep 2023 16:27:38 GMT", 2],
[3, "99999999999999999999999999999999999", 2],
[3, "Zun, 29 Sep 2023 16:26:27 GMT", 2],
[3, "", 2],
[3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5],
[3, "99999999999999999999999999999999999", 0.5],
[3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5],
[3, "", 0.5],
[2, "", 0.5 * 2.0],
[1, "", 0.5 * 4.0],
],
)
@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
Expand All @@ -749,9 +751,9 @@ def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str
)

headers = httpx.Headers({"retry-after": retry_after})
options = FinalRequestOptions(method="get", url="/foo", max_retries=2)
options = FinalRequestOptions(method="get", url="/foo", max_retries=3)
calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
assert calculated == pytest.approx(timeout, 0.6) # pyright: ignore[reportUnknownMemberType]
assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType]


class TestAsyncModernTreasury:
Expand Down Expand Up @@ -1448,18 +1450,20 @@ class Model(BaseModel):
"remaining_retries,retry_after,timeout",
[
[3, "20", 20],
[3, "0", 2],
[3, "-10", 2],
[3, "0", 0.5],
[3, "-10", 0.5],
[3, "60", 60],
[3, "61", 2],
[3, "61", 0.5],
[3, "Fri, 29 Sep 2023 16:26:57 GMT", 20],
[3, "Fri, 29 Sep 2023 16:26:37 GMT", 2],
[3, "Fri, 29 Sep 2023 16:26:27 GMT", 2],
[3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5],
[3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5],
[3, "Fri, 29 Sep 2023 16:27:37 GMT", 60],
[3, "Fri, 29 Sep 2023 16:27:38 GMT", 2],
[3, "99999999999999999999999999999999999", 2],
[3, "Zun, 29 Sep 2023 16:26:27 GMT", 2],
[3, "", 2],
[3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5],
[3, "99999999999999999999999999999999999", 0.5],
[3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5],
[3, "", 0.5],
[2, "", 0.5 * 2.0],
[1, "", 0.5 * 4.0],
],
)
@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
Expand All @@ -1470,6 +1474,6 @@ async def test_parse_retry_after_header(self, remaining_retries: int, retry_afte
)

headers = httpx.Headers({"retry-after": retry_after})
options = FinalRequestOptions(method="get", url="/foo", max_retries=2)
options = FinalRequestOptions(method="get", url="/foo", max_retries=3)
calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
assert calculated == pytest.approx(timeout, 0.6) # pyright: ignore[reportUnknownMemberType]
assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType]

0 comments on commit 7dcfdd8

Please sign in to comment.