fix(pmc): не давать залипшему запросу съесть весь бюджет прогона

Таймаут запроса к бакету был 60с при бюджете таска 1500с — один
залипший GET отъедал почти весь прогон. Плюс map() отдаёт результаты
строго по порядку отправки, поэтому один медленный запрос блокировал
все 11 уже готовых потоков.

Таймаут снижен до 12с (обычный GET укладывается в доли секунды, 12с —
запас на джиттер), map() заменён на as_completed: готовые результаты
отдаются сразу, не дожидаясь залипшего соседа.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jze9
2026-09-15 13:39:45 +05:00
parent 5ceec1e2e3
commit 20c1d00443

View File

@@ -14,7 +14,7 @@
import logging import logging
import re import re
from collections.abc import Iterator from collections.abc import Iterator
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Any from typing import Any
from urllib.parse import quote from urllib.parse import quote
@@ -39,8 +39,11 @@ class PMCBulkParser(BaseParser):
def __init__(self, workers: int = 12) -> None: def __init__(self, workers: int = 12) -> None:
super().__init__() super().__init__()
self.workers = workers self.workers = workers
# 60с на файл при бюджете задачи в 1500с — один залипший запрос съедал
# почти весь бюджет. Обычный GET сюда укладывается в доли секунды,
# 12с — с большим запасом на джиттер, но без риска съесть весь прогон
self.client = httpx.Client( self.client = httpx.Client(
timeout=60, timeout=12,
headers={"User-Agent": "AcademicHelper/1.0 (noreply@jze9.ru)"}, headers={"User-Agent": "AcademicHelper/1.0 (noreply@jze9.ru)"},
) )
# Позиция листинга: бакет отдаётся страницами, и продолжать прогон # Позиция листинга: бакет отдаётся страницами, и продолжать прогон
@@ -82,8 +85,13 @@ class PMCBulkParser(BaseParser):
logger.info("PMC bulk: бакет закончился") logger.info("PMC bulk: бакет закончился")
return return
# as_completed вместо map(): map() отдаёт результаты строго по
# порядку отправки, поэтому один залипший запрос блокирует все
# уже готовые — даже если остальные 11 потоков давно отработали
with ThreadPoolExecutor(max_workers=self.workers) as pool: with ThreadPoolExecutor(max_workers=self.workers) as pool:
for raw in pool.map(self._fetch_article, ids): futures = {pool.submit(self._fetch_article, i): i for i in ids}
for future in as_completed(futures):
raw = future.result()
if raw is None: if raw is None:
continue continue
given += 1 given += 1