Stabilize worker sync flow
This commit is contained in:
@@ -21,7 +21,7 @@ from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
|
||||
|
||||
from .browser import BrowserFactory, HumanPacer, NetworkCapture
|
||||
from .core.config import Settings, settings, parse_listing_segments
|
||||
from .core.exceptions import AntiBotDetectedError, SiteStructureChangedError
|
||||
from .core.exceptions import AntiBotDetectedError, ListingResumeError, SiteStructureChangedError
|
||||
from .core.logs import set_trace_id, setup_logging
|
||||
from .core.retry import retryable
|
||||
from .core.runtime_config import RuntimeConfig
|
||||
@@ -310,6 +310,30 @@ class IAAIScraper:
|
||||
page_result = self.listing_collector.collect_current_page(page, page_number=page_number)
|
||||
return page_result, self._extract_page_urls(page_result, all_raw_urls, seen_urls, all_listing_origin_urls)
|
||||
|
||||
def _open_listing_page(
|
||||
self,
|
||||
*,
|
||||
make: str | None,
|
||||
model: str | None,
|
||||
listing_url: str | None = None,
|
||||
year_min: int | None = None,
|
||||
year_max: int | None = None,
|
||||
) -> tuple[Page, dict[str, str | int | None]]:
|
||||
page = self._get_page_with_warmup()
|
||||
try:
|
||||
self.listing_collector.open_cars_listing(page, url_override=listing_url)
|
||||
applied_filters = self.listing_collector.apply_filters(
|
||||
page,
|
||||
make=make,
|
||||
model=model,
|
||||
year_min=year_min,
|
||||
year_max=year_max,
|
||||
)
|
||||
except Exception:
|
||||
page.close()
|
||||
raise
|
||||
return page, applied_filters
|
||||
|
||||
def _reopen_listing_and_resume(
|
||||
self,
|
||||
*,
|
||||
@@ -320,28 +344,103 @@ class IAAIScraper:
|
||||
year_min: int | None = None,
|
||||
year_max: int | None = None,
|
||||
max_nav_pages: int = 10,
|
||||
) -> Page:
|
||||
) -> tuple[Page, dict[str, str | int | None]]:
|
||||
# Ограничиваем глубину навигации: если до цели > max_nav_pages кликов — не пытаемся.
|
||||
if target_page_number > max_nav_pages + 1:
|
||||
raise RuntimeError(
|
||||
raise ListingResumeError(
|
||||
f"Cannot resume at page {target_page_number}: "
|
||||
f"exceeds max navigation depth ({max_nav_pages} pages)"
|
||||
)
|
||||
page = self._get_page_with_warmup()
|
||||
try:
|
||||
self.listing_collector.open_cars_listing(page, url_override=listing_url)
|
||||
self.listing_collector.apply_filters(page, make=make, model=model, year_min=year_min, year_max=year_max)
|
||||
except Exception:
|
||||
page.close()
|
||||
raise
|
||||
page, applied_filters = self._open_listing_page(
|
||||
make=make,
|
||||
model=model,
|
||||
listing_url=listing_url,
|
||||
year_min=year_min,
|
||||
year_max=year_max,
|
||||
)
|
||||
|
||||
for expected_page in range(2, target_page_number + 1):
|
||||
if not self.listing_collector.go_to_next_page(page, expected_page_number=expected_page):
|
||||
page.close()
|
||||
raise RuntimeError(f"Failed to resume listing at page {target_page_number}")
|
||||
raise ListingResumeError(f"Failed to resume listing at page {target_page_number}")
|
||||
|
||||
logger.warning("Listing resumed at page %d after recovery", target_page_number)
|
||||
return page
|
||||
return page, applied_filters
|
||||
|
||||
def _reset_listing_progress_checkpoint(
|
||||
self,
|
||||
progress_callback: Callable[[int], None] | None,
|
||||
*,
|
||||
target_page_number: int,
|
||||
reason: str,
|
||||
) -> None:
|
||||
if progress_callback is None:
|
||||
return
|
||||
try:
|
||||
# page_number=0 — специальный sentinel: следующий запуск должен начать scope с page 1.
|
||||
progress_callback(0)
|
||||
logger.warning(
|
||||
"Reset listing checkpoint after failed resume to page %d: %s",
|
||||
target_page_number,
|
||||
reason,
|
||||
)
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"Failed to reset listing checkpoint after resume failure to page %d",
|
||||
target_page_number,
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
def _open_listing_for_stream(
|
||||
self,
|
||||
*,
|
||||
start_page: int,
|
||||
make: str | None,
|
||||
model: str | None,
|
||||
progress_callback: Callable[[int], None] | None,
|
||||
listing_url: str | None = None,
|
||||
year_min: int | None = None,
|
||||
year_max: int | None = None,
|
||||
) -> tuple[Page, dict[str, str | int | None], int]:
|
||||
if start_page <= 1:
|
||||
page, applied_filters = self._open_listing_page(
|
||||
make=make,
|
||||
model=model,
|
||||
listing_url=listing_url,
|
||||
year_min=year_min,
|
||||
year_max=year_max,
|
||||
)
|
||||
return page, applied_filters, 1
|
||||
|
||||
try:
|
||||
page, applied_filters = self._reopen_listing_and_resume(
|
||||
target_page_number=start_page,
|
||||
make=make,
|
||||
model=model,
|
||||
listing_url=listing_url,
|
||||
year_min=year_min,
|
||||
year_max=year_max,
|
||||
)
|
||||
return page, applied_filters, start_page
|
||||
except ListingResumeError as resume_exc:
|
||||
logger.warning(
|
||||
"Stored checkpoint page %d is unreachable for current listing scope; restarting scope from page 1: %s",
|
||||
start_page,
|
||||
resume_exc,
|
||||
)
|
||||
self._reset_listing_progress_checkpoint(
|
||||
progress_callback,
|
||||
target_page_number=start_page,
|
||||
reason=str(resume_exc),
|
||||
)
|
||||
page, applied_filters = self._open_listing_page(
|
||||
make=make,
|
||||
model=model,
|
||||
listing_url=listing_url,
|
||||
year_min=year_min,
|
||||
year_max=year_max,
|
||||
)
|
||||
return page, applied_filters, 1
|
||||
|
||||
def collect_listing(
|
||||
self,
|
||||
@@ -518,26 +617,19 @@ class IAAIScraper:
|
||||
failures.append({"vehicle_url": f"batch_{batch_start}", "error": str(batch_exc)})
|
||||
return True
|
||||
|
||||
page = self._get_page_with_warmup() if start_page <= 1 else None
|
||||
page = None
|
||||
try:
|
||||
if start_page <= 1:
|
||||
assert page is not None
|
||||
self.listing_collector.open_cars_listing(page, url_override=listing_url)
|
||||
applied_filters = self.listing_collector.apply_filters(
|
||||
page, make=make, model=model, year_min=year_min, year_max=year_max,
|
||||
)
|
||||
else:
|
||||
page = self._reopen_listing_and_resume(
|
||||
target_page_number=start_page,
|
||||
make=make,
|
||||
model=model,
|
||||
listing_url=listing_url,
|
||||
year_min=year_min,
|
||||
year_max=year_max,
|
||||
)
|
||||
applied_filters = {"make": make, "model": model, "year_min": year_min, "year_max": year_max}
|
||||
page, applied_filters, effective_start_page = self._open_listing_for_stream(
|
||||
start_page=start_page,
|
||||
make=make,
|
||||
model=model,
|
||||
progress_callback=progress_callback,
|
||||
listing_url=listing_url,
|
||||
year_min=year_min,
|
||||
year_max=year_max,
|
||||
)
|
||||
|
||||
for page_number in range(start_page, max(start_page, self.settings.listing.max_pages_per_run) + 1):
|
||||
for page_number in range(effective_start_page, max(effective_start_page, self.settings.listing.max_pages_per_run) + 1):
|
||||
page_result = self.listing_collector.collect_current_page(page, page_number=page_number)
|
||||
pages_info.append({
|
||||
"page_number": page_result.page_number,
|
||||
@@ -564,7 +656,7 @@ class IAAIScraper:
|
||||
logger.warning("Page %d still empty after retry — reopening listing and resuming", page_number)
|
||||
page.close()
|
||||
try:
|
||||
page = self._reopen_listing_and_resume(
|
||||
page, _ = self._reopen_listing_and_resume(
|
||||
target_page_number=page_number,
|
||||
make=make,
|
||||
model=model,
|
||||
@@ -579,12 +671,17 @@ class IAAIScraper:
|
||||
seen_urls,
|
||||
all_listing_origin_urls,
|
||||
)
|
||||
except RuntimeError as resume_exc:
|
||||
except ListingResumeError as resume_exc:
|
||||
logger.warning(
|
||||
"Cannot resume at page %d (%s) — stopping pagination for this segment",
|
||||
"Cannot resume at page %d (%s) — resetting checkpoint and stopping pagination for this segment",
|
||||
page_number, resume_exc,
|
||||
)
|
||||
page = self._get_page_with_warmup()
|
||||
self._reset_listing_progress_checkpoint(
|
||||
progress_callback,
|
||||
target_page_number=page_number,
|
||||
reason=str(resume_exc),
|
||||
)
|
||||
page = None
|
||||
page_urls = []
|
||||
if not page_urls:
|
||||
logger.info("Page %d: 0 new links, stopping pagination", page_number)
|
||||
@@ -670,8 +767,9 @@ class IAAIScraper:
|
||||
if not self.listing_collector.go_to_next_page(page, expected_page_number=page_number + 1):
|
||||
logger.warning("Failed to navigate to page %d — reopening listing and resuming", page_number + 1)
|
||||
page.close()
|
||||
page = None
|
||||
try:
|
||||
page = self._reopen_listing_and_resume(
|
||||
page, _ = self._reopen_listing_and_resume(
|
||||
target_page_number=page_number + 1,
|
||||
make=make,
|
||||
model=model,
|
||||
@@ -679,12 +777,16 @@ class IAAIScraper:
|
||||
year_min=year_min,
|
||||
year_max=year_max,
|
||||
)
|
||||
except RuntimeError as resume_exc:
|
||||
except ListingResumeError as resume_exc:
|
||||
logger.warning(
|
||||
"Cannot resume at page %d (%s) — stopping pagination for this segment",
|
||||
"Cannot resume at page %d (%s) — resetting checkpoint and treating pagination as exhausted",
|
||||
page_number + 1, resume_exc,
|
||||
)
|
||||
page = self._get_page_with_warmup()
|
||||
self._reset_listing_progress_checkpoint(
|
||||
progress_callback,
|
||||
target_page_number=page_number + 1,
|
||||
reason=str(resume_exc),
|
||||
)
|
||||
break
|
||||
|
||||
if pending_urls:
|
||||
@@ -1646,11 +1748,15 @@ class IAAIScraper:
|
||||
"segment": seg,
|
||||
"segment_index": seg_idx,
|
||||
"status": result.get("status"),
|
||||
"full_scan_completed": bool(result.get("full_scan_completed", False)),
|
||||
"cars_upserted": result.get("cars_upserted", 0),
|
||||
"cars_failed": result.get("cars_failed", 0),
|
||||
"vehicles_collected": result.get("listing", {}).get("vehicles_collected", 0),
|
||||
})
|
||||
|
||||
if not bool(result.get("full_scan_completed", False)):
|
||||
completed_all = False
|
||||
|
||||
logger.warning(
|
||||
"Segment %d/%d done: %s → upserted=%d, failed=%d, collected=%d",
|
||||
seg_idx + 1, len(segments), seg_label,
|
||||
|
||||
Reference in New Issue
Block a user