fix(clips): RedGifs 1 zapytanie/s, odstep z error.delay przy 429, przerwanie backfillu zamiast mielenia nisz
Pierwszy backfill po niszach przy ~3 zapytaniach/s po ~65 zapytaniach wpadl w trwale 429 (cialo: delay 1167 s, subject per IP i endpoint) i przemielil 40 kolejnych nisz w bledach. Teraz: 1/s, odstep z Retry-After albo error.delay, powyzej 5 min RateLimited i koniec przebiegu (wznowienie --skip).
This commit is contained in:
parent
4f5de92bfc
commit
cd4803ed51
2 changed files with 46 additions and 7 deletions
|
|
@ -17,7 +17,10 @@ globalne „najnowsze" (98-100 na stronę). `order` ∈ latest|top|top7|top28|tr
|
|||
NIE `new` (400 BadOrder). Każde zapytanie widzi maks. 10 000 wyników (`pages` ≤ 100,
|
||||
strona 101 → 500), więc pełny katalog chodzi się po niszach (`/v2/niches`, 1812
|
||||
społeczności, `/v2/niches/<id>/gifs`) i po twórcach (`/v2/users/<name>/search`).
|
||||
Brak nagłówków rate-limit; trzymamy ~3 zapytania/s i ustępujemy przy 429.
|
||||
Limit: per IP i per endpoint (`subject: "<ip>|niche-gifs|<n>"` w ciele 429), bez nagłówka
|
||||
Retry-After, za to w ciele `error.delay` w sekundach (widziane 1167 s po ~65 zapytaniach
|
||||
w 20 s). Trzymamy 1 zapytanie/s; przy 429 czekamy `delay`, a gdy jest dłuższy niż 5 min,
|
||||
przerywamy przebieg (`RateLimited`), bo dalsze zapytania tylko przedłużają blokadę.
|
||||
|
||||
**Rekord gifa** (pola, które bierzemy): `id`, `type` (1 = wideo, 2 = obraz, pomijamy),
|
||||
`urls{hd,sd,poster,thumbnail}`, `duration`, `width/height`, `hasAudio`, `hls`, `tags[]`,
|
||||
|
|
@ -48,7 +51,12 @@ _HEADERS = {
|
|||
"Referer": "https://www.redgifs.com/",
|
||||
}
|
||||
_PAGE_MAX = 100
|
||||
_MIN_INTERVAL_S = 0.35
|
||||
#: Pomiar 2026-09-14: przy ~3 zapytaniach/s po ~65 zapytaniach z rzedu RedGifs oddaje 429
|
||||
#: z `delay` ~20 min. 1 zapytanie/s ma sie trzymac w limicie (do weryfikacji na backfillu).
|
||||
_MIN_INTERVAL_S = 1.0
|
||||
_RATE_LIMIT_BACKOFF_S = (30, 90, 180)
|
||||
#: Dluzsze `delay` z ciala 429 nie czekamy w procesie, tylko przerywamy przebieg.
|
||||
_RATE_LIMIT_MAX_WAIT_S = 300
|
||||
_TAG_SLUG_RE = re.compile(r"[^a-z0-9]+")
|
||||
|
||||
|
||||
|
|
@ -128,8 +136,27 @@ def map_gif(g: dict) -> RawClip | None:
|
|||
)
|
||||
|
||||
|
||||
def _rate_limit_wait(r: httpx.Response, attempt: int) -> float:
|
||||
"""Odstęp po 429: `Retry-After`, potem `error.delay` z ciała, potem stała drabinka."""
|
||||
retry_after = r.headers.get("Retry-After")
|
||||
if retry_after and retry_after.isdigit():
|
||||
return float(retry_after)
|
||||
try:
|
||||
delay = (r.json() or {}).get("error", {}).get("delay")
|
||||
if isinstance(delay, (int, float)) and delay > 0:
|
||||
return float(delay)
|
||||
except ValueError:
|
||||
pass
|
||||
return float(_RATE_LIMIT_BACKOFF_S[min(attempt, len(_RATE_LIMIT_BACKOFF_S) - 1)])
|
||||
|
||||
|
||||
class RateLimited(RuntimeError):
|
||||
"""429 mimo odczekania: wołający ma przerwać przebieg, nie iść do następnej niszy
|
||||
(każde kolejne zapytanie tylko przedłuża blokadę)."""
|
||||
|
||||
|
||||
class RedgifsClient:
|
||||
"""Cienki klient z tokenem tymczasowym i odnowieniem na 401."""
|
||||
"""Cienki klient z tokenem tymczasowym, odnowieniem na 401 i odczekiwaniem 429."""
|
||||
|
||||
def __init__(self, *, timeout: float = 30.0) -> None:
|
||||
self._http = httpx.Client(headers=_HEADERS, timeout=timeout)
|
||||
|
|
@ -153,18 +180,24 @@ class RedgifsClient:
|
|||
if wait > 0:
|
||||
time.sleep(wait)
|
||||
tok = self._token or self._auth()
|
||||
for attempt in range(3):
|
||||
for attempt in range(1 + len(_RATE_LIMIT_BACKOFF_S)):
|
||||
r = self._http.get(f"{_BASE}{path}", params=params, headers={"Authorization": f"Bearer {tok}"})
|
||||
self._last_call = time.monotonic()
|
||||
if r.status_code == 401 and attempt == 0:
|
||||
tok = self._auth()
|
||||
continue
|
||||
if r.status_code == 429:
|
||||
time.sleep(5 * (attempt + 1))
|
||||
if attempt >= len(_RATE_LIMIT_BACKOFF_S):
|
||||
raise RateLimited(f"redgifs: 429 na {path} po {attempt} odczekaniach")
|
||||
wait = _rate_limit_wait(r, attempt)
|
||||
if wait > _RATE_LIMIT_MAX_WAIT_S:
|
||||
raise RateLimited(f"redgifs: 429 na {path}, zadany odstep {wait:.0f} s")
|
||||
log.info("redgifs: 429 na %s, czekam %.0f s", path, wait)
|
||||
time.sleep(wait)
|
||||
continue
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
raise RuntimeError(f"redgifs: {path} nie odpowiedzial poprawnie po 3 probach")
|
||||
raise RuntimeError(f"redgifs: {path} nie odpowiedzial poprawnie")
|
||||
|
||||
# ---- listingi ---------------------------------------------------------------------
|
||||
def latest(self, page: int = 1, count: int = _PAGE_MAX) -> list[dict]:
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ from sqlalchemy import text
|
|||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.connectors.clips.redgifs import RawClip, RedgifsClient, map_gif
|
||||
from app.connectors.clips.redgifs import RateLimited, RawClip, RedgifsClient, map_gif
|
||||
from app.db import session_scope
|
||||
from app.models.clip import Clip, ClipTag
|
||||
|
||||
|
|
@ -148,6 +148,12 @@ def run_redgifs_niches(
|
|||
for page in range(1, per_niche_pages + 1):
|
||||
try:
|
||||
gifs = client.niche_gifs(nid, order=order, page=page)
|
||||
except RateLimited as e:
|
||||
# Limit trwaly: koniec przebiegu, wznowienie przez --skip <niches>.
|
||||
# Pierwszy backfill (2026-09-14) mielil 40 nisz po kolei w 429.
|
||||
log.warning("clips/redgifs niches: %s; przerywam po %d niszach", e, total["niches"])
|
||||
total["errors"] += 1
|
||||
return total
|
||||
except Exception as e:
|
||||
log.warning("clips/redgifs niche %s strona %d padla: %s", nid, page, e)
|
||||
total["errors"] += 1
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue