Skip to content

Commit

Permalink
feat(client): adjust retry behavior to be exponential backoff (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
stainless-bot authored Oct 24, 2023
1 parent e0ec9fc commit 6ec6a72
Show file tree
Hide file tree
Showing 2 changed files with 31 additions and 27 deletions.
10 changes: 5 additions & 5 deletions src/orb/_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 @@ -539,28 +539,30 @@ 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))
def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None:
client = Orb(base_url=base_url, api_key=api_key, _strict_response_validation=True)

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 TestAsyncOrb:
Expand Down Expand Up @@ -1083,18 +1085,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 @@ -1103,6 +1107,6 @@ async def test_parse_retry_after_header(self, remaining_retries: int, retry_afte
client = AsyncOrb(base_url=base_url, api_key=api_key, _strict_response_validation=True)

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 6ec6a72

Please sign in to comment.