Repository navigation
fix(api): jitter the failover backoff, and retry a 503 only with Retry-After #866
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -14,15 +14,18 @@ | |||||
|
|
||||||
| """Region failover for the Twirp API clients. | ||||||
|
|
||||||
| On a retryable failure (any transport error or HTTP 5xx) the client discovers | ||||||
| On a retryable failure (any transport error, or an HTTP 5xx other than a 503 | ||||||
| without Retry-After) the client discovers | ||||||
| alternative LiveKit Cloud regions via ``/settings/regions`` and replays the | ||||||
| request against the next region, with exponential backoff. 4xx responses are | ||||||
| request against the next region, with jittered exponential backoff. 4xx responses are | ||||||
| returned immediately. | ||||||
| """ | ||||||
|
|
||||||
| from __future__ import annotations | ||||||
|
|
||||||
| import random | ||||||
| import time | ||||||
| from email.utils import parsedate_to_datetime | ||||||
| from dataclasses import dataclass | ||||||
| from typing import Dict, List, Optional | ||||||
| from urllib.parse import urlparse | ||||||
|
|
@@ -66,6 +69,26 @@ def is_cloud_api(host: str) -> bool: | |||||
| return host.startswith("cloud-api.") and host.endswith(".livekit.io") | ||||||
|
|
||||||
|
|
||||||
| def backoff(attempt: int, base: float) -> float: | ||||||
| """Full jitter: a uniform delay in [0, base * 2^attempt) so clients that | ||||||
| failed together don't retry in lockstep.""" | ||||||
| return random.random() * base * (2**attempt) | ||||||
|
|
||||||
|
|
||||||
| def parse_retry_after(header: Optional[str]) -> Optional[float]: | ||||||
| """Parses a Retry-After header (delay-seconds or an HTTP-date) to seconds.""" | ||||||
| value = (header or "").strip() | ||||||
| if not value: | ||||||
| return None | ||||||
| if value.isdigit(): | ||||||
| return float(value) | ||||||
| try: | ||||||
| date = parsedate_to_datetime(value) | ||||||
| except (TypeError, ValueError): | ||||||
| return None | ||||||
| return max(0.0, date.timestamp() - time.time()) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Asctime retry dates use local time For an asctime Learn moreHTTP-date accepts the asctime form, which has no timezone suffix but still denotes GMT. Example: With the client in New York at 03:25:35 UTC, Recommended fix: In
Suggested change
Was this helpful? React with 👍 or 👎 to provide feedback. |
||||||
|
|
||||||
|
|
||||||
| def to_http(url: str) -> str: | ||||||
| """Normalizes a region URL to an http(s) scheme (ws -> http, wss -> https).""" | ||||||
| if url.startswith("ws"): | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Digit-shaped retry header crashes API calls
When a response contains
Retry-After: ²,parse_retry_afterraisesValueErrorbefore the client handles its status.isdigit()accepts that character, butfloat()rejects it; even a terminal 400 response loses itsServerError.Learn more
The client parses
Retry-Afteron every non-200 response before deciding whether to retry. Python'sisdigit()accepts characters like², althoughfloat()cannot parse them. An unexpected header therefore escapes asValueErrorrather than producing the response'sServerError.Example: An API returns HTTP 400 with
Retry-After: ². Instead of surfacing status 400,TwirpClient.requestraisesValueErrorfrom the parser.Recommended fix: Restrict delay-seconds to ASCII decimal digits before converting, and treat all other strings as dates or invalid headers. Test invalid Unicode digits on both terminal and retryable statuses.
Was this helpful? React with 👍 or 👎 to provide feedback.