Fail fast on OpenDota 429 in hero_matches fetch.
Stop the long retry chain after three consecutive 429s and restore prior hero cells so a burned IP no longer stalls the whole daily tier. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -61,6 +61,34 @@ from shared.http_utils import download_icons, http_json, write_json_atomic
|
||||
from shared.paths import DATA, ITEM_ICONS
|
||||
|
||||
OPENDOTA = "https://api.opendota.com/api"
|
||||
# Consecutive OpenDota 429s before aborting the remaining hero batch.
|
||||
_429_STREAK = 0
|
||||
_429_STREAK_LIMIT = 3
|
||||
|
||||
|
||||
class RateLimitTripped(Exception):
|
||||
"""Enough consecutive OpenDota 429s to stop the rest of this run."""
|
||||
|
||||
|
||||
def _opendota_json(url: str):
|
||||
"""Fail-fast OpenDota JSON (no 5/10/20/40s retry chain on 429)."""
|
||||
global _429_STREAK
|
||||
try:
|
||||
data = http_json(url, retries=0)
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 429:
|
||||
_429_STREAK += 1
|
||||
print(
|
||||
f"HTTP 429 {url} — streak {_429_STREAK}/{_429_STREAK_LIMIT}",
|
||||
flush=True,
|
||||
)
|
||||
if _429_STREAK >= _429_STREAK_LIMIT:
|
||||
raise RateLimitTripped(
|
||||
f"opendota 429 x{_429_STREAK}"
|
||||
) from e
|
||||
raise
|
||||
_429_STREAK = 0
|
||||
return data
|
||||
ABILITY_IDS_URL = (
|
||||
"https://raw.githubusercontent.com/odota/dotaconstants/master/build/ability_ids.json"
|
||||
)
|
||||
@@ -605,7 +633,9 @@ def opendota_url(path: str) -> str:
|
||||
|
||||
def fetch_match(match_id: int) -> dict | None:
|
||||
try:
|
||||
raw = http_json(opendota_url(f"/matches/{match_id}"))
|
||||
raw = _opendota_json(opendota_url(f"/matches/{match_id}"))
|
||||
except RateLimitTripped:
|
||||
raise
|
||||
except (
|
||||
urllib.error.HTTPError,
|
||||
urllib.error.URLError,
|
||||
@@ -620,7 +650,9 @@ def fetch_match(match_id: int) -> dict | None:
|
||||
def league_match_ids(hero_id: int, limit: int) -> list[dict]:
|
||||
"""Return win list metas from /heroes/{id}/matches (up to candidate cap)."""
|
||||
try:
|
||||
raw = http_json(opendota_url(f"/heroes/{hero_id}/matches"))
|
||||
raw = _opendota_json(opendota_url(f"/heroes/{hero_id}/matches"))
|
||||
except RateLimitTripped:
|
||||
raise
|
||||
except (
|
||||
urllib.error.HTTPError,
|
||||
urllib.error.URLError,
|
||||
@@ -700,7 +732,9 @@ def public_match_ids(
|
||||
sep = "&" if "?" in url else "?"
|
||||
url = f"{url}{sep}less_than_match_id={less_than}"
|
||||
try:
|
||||
raw = http_json(url)
|
||||
raw = _opendota_json(url)
|
||||
except RateLimitTripped:
|
||||
raise
|
||||
except (
|
||||
urllib.error.HTTPError,
|
||||
urllib.error.URLError,
|
||||
@@ -1401,6 +1435,27 @@ def main() -> None:
|
||||
public_region=public_region,
|
||||
workers=workers,
|
||||
)
|
||||
except RateLimitTripped as exc:
|
||||
print(
|
||||
f" rate-limited; keeping prior cache for remaining ({exc})",
|
||||
flush=True,
|
||||
)
|
||||
prior = existing.get("by_hero") if isinstance(existing, dict) else {}
|
||||
if not isinstance(prior, dict):
|
||||
prior = {}
|
||||
for rest_key in pending[i - 1 :]:
|
||||
prev = prior.get(rest_key)
|
||||
if prev is not None:
|
||||
by_hero[rest_key] = prev
|
||||
write_out(
|
||||
args.out,
|
||||
by_hero,
|
||||
source=args.source,
|
||||
limit=limit,
|
||||
item_catalog=item_catalog,
|
||||
public_region=public_region,
|
||||
)
|
||||
break
|
||||
except (
|
||||
urllib.error.HTTPError,
|
||||
urllib.error.URLError,
|
||||
|
||||
Reference in New Issue
Block a user