fix(openalex): пауза между страницами и живой backoff — заливка тонула в 429
Массовый запуск показал: лимит вежливого пула OpenAlex (10 req/s) общий на mailto, а не на процесс. Четыре воркера с паузой 0.1с получали сплошные 429, и каждый прогон уходил в 900с бесполезного backoff, не забрав ничего. - RATE_LIMIT_DELAY 0.1 → 1.0с (≈4 req/s на четырёх воркерах); - backoff спит кусками по 5с и отчитывается через progress_cb: прогон больше не выглядит зависшим в админке и отменяется во время ожидания, а не после. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,35 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
OPENALEX_API = "https://api.openalex.org"
|
||||
DEFAULT_EMAIL = "noreply@jze9.ru" # Для вежливого агента
|
||||
# Пауза между страницами. Лимит вежливого пула — 10 запросов/сек НА ВЕСЬ ключ
|
||||
# (mailto), а не на процесс: четыре воркера, качающие разные источники, делят
|
||||
# его между собой. С прежними 0.1с массовая заливка утыкалась в сплошные 429 и
|
||||
# каждый прогон уходил в 900с бесполезного backoff. 1с × 4 воркера ≈ 4 req/s.
|
||||
RATE_LIMIT_DELAY = 1.0
|
||||
# Backoff режем на куски: во время сна парсер обязан отчитываться о жизни,
|
||||
# иначе прогон выглядит зависшим и его нельзя отменить из админки.
|
||||
BACKOFF_TICK_S = 5.0
|
||||
|
||||
|
||||
def _sleep_alive(
|
||||
seconds: float, progress_cb: ProgressCallback | None, fetched: int
|
||||
) -> bool:
|
||||
"""Поспать, отчитываясь о жизни; False — попросили остановиться.
|
||||
|
||||
Минуты сна в backoff нельзя проводить молча: для админки такой прогон
|
||||
неотличим от зависшего, а отмена не сработает до конца ожидания.
|
||||
"""
|
||||
if progress_cb is None:
|
||||
time.sleep(seconds)
|
||||
return True
|
||||
|
||||
left = seconds
|
||||
while left > 0:
|
||||
time.sleep(min(BACKOFF_TICK_S, left))
|
||||
left -= BACKOFF_TICK_S
|
||||
if not progress_cb(fetched):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class OpenAlexParser(BaseParser):
|
||||
@@ -74,6 +103,7 @@ class OpenAlexParser(BaseParser):
|
||||
year_to=year_to,
|
||||
type_filter=type_filter,
|
||||
open_access_only=open_access_only,
|
||||
progress_cb=progress_cb,
|
||||
):
|
||||
results.extend(page)
|
||||
if progress_cb and not progress_cb(min(len(results), limit)):
|
||||
@@ -93,6 +123,7 @@ class OpenAlexParser(BaseParser):
|
||||
year_to: int | None,
|
||||
type_filter: str,
|
||||
open_access_only: bool = False,
|
||||
progress_cb: ProgressCallback | None = None,
|
||||
) -> Generator[list[dict], None, None]:
|
||||
"""Cursor-based пагинация OpenAlex."""
|
||||
cursor = "*"
|
||||
@@ -155,8 +186,7 @@ class OpenAlexParser(BaseParser):
|
||||
if not cursor:
|
||||
break
|
||||
|
||||
# Rate limiting: 10 запросов/сек без ключа
|
||||
time.sleep(0.1)
|
||||
time.sleep(RATE_LIMIT_DELAY)
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"OpenAlex HTTP ошибка: {e.response.status_code}")
|
||||
@@ -173,7 +203,9 @@ class OpenAlexParser(BaseParser):
|
||||
# лимиту освободиться, каждый воркер продлевал блокировку сам.
|
||||
wait = 60 * (2 ** (rate_limit_retries - 1))
|
||||
logger.warning(f"Rate limit! Попытка {rate_limit_retries}/{MAX_RATE_LIMIT_RETRIES}, ждём {wait}с...")
|
||||
time.sleep(wait)
|
||||
if not _sleep_alive(wait, progress_cb, total_fetched):
|
||||
logger.info("OpenAlex: ожидание прервано по запросу")
|
||||
break
|
||||
continue
|
||||
break
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user