Merge pull request 'fix(avito): gate 403 reconnect-retry on proxy presence only (was dead code in prod)' (#1765) from fix/avito-403-reconnect-gate into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 6s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 1m6s
Deploy Trade-In / build-backend (push) Successful in 50s
Deploy Trade-In / deploy (push) Successful in 45s
All checks were successful
Deploy Trade-In / changes (push) Successful in 6s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 1m6s
Deploy Trade-In / build-backend (push) Successful in 50s
Deploy Trade-In / deploy (push) Successful in 45s
Reviewed-on: #1765
This commit is contained in:
commit
c58c5a5aab
2 changed files with 66 additions and 15 deletions
|
|
@ -437,9 +437,11 @@ class AvitoScraper(BaseScraper):
|
||||||
return html
|
return html
|
||||||
assert self._cffi is not None
|
assert self._cffi is not None
|
||||||
max_rot = settings.avito_proxy_max_rotations if settings.avito_proxy_rotate_url else 0
|
max_rot = settings.avito_proxy_max_rotations if settings.avito_proxy_rotate_url else 0
|
||||||
# backconnect-режим: прокси задан, но changeip-URL нет → ротация exit-IP
|
# curl_cffi-сессия всегда egress через settings.scraper_proxy_url (backconnect
|
||||||
# делается пересозданием соединения, а не changeip-вызовом.
|
# mproxy). Пересоздание сессии (_reset_cffi) даёт свежий exit-IP вне зависимости
|
||||||
backconnect = bool(settings.scraper_proxy_url) and not settings.avito_proxy_rotate_url
|
# от наличия changeip-URL (тот относится к ДРУГОМУ, browser/auv-прокси). Поэтому
|
||||||
|
# reconnect-retry гейтим ТОЛЬКО на наличие proxy, не на отсутствие rotate_url.
|
||||||
|
backconnect = bool(settings.scraper_proxy_url)
|
||||||
rot_done = 0
|
rot_done = 0
|
||||||
transient_left = 2
|
transient_left = 2
|
||||||
r429 = 0
|
r429 = 0
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,10 @@
|
||||||
mark_banned. Каждое новое соединение через backconnect даёт свежий exit-IP, поэтому
|
mark_banned. Каждое новое соединение через backconnect даёт свежий exit-IP, поэтому
|
||||||
ретраим пересозданием curl_cffi-сессии до _AVITO_403_MAX_RETRIES раз.
|
ретраим пересозданием curl_cffi-сессии до _AVITO_403_MAX_RETRIES раз.
|
||||||
|
|
||||||
backconnect = scraper_proxy_url задан И avito_proxy_rotate_url НЕ задан. Включаем через
|
curl_cffi-сессия всегда egress через scraper_proxy_url (backconnect mproxy), поэтому
|
||||||
scraper_proxy_url_env (scraper_proxy_url — read-only property, его не monkeypatch'ишь).
|
reconnect-retry гейтим ТОЛЬКО на наличие proxy — независимо от того, задан ли changeip-URL
|
||||||
|
(тот относится к ДРУГОМУ, browser/auv-прокси). Включаем через scraper_proxy_url_env
|
||||||
|
(scraper_proxy_url — read-only property, его не monkeypatch'ишь).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
@ -136,13 +138,15 @@ async def test_backconnect_403_exhausted_raises_blocked(
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_non_backconnect_with_rotate_url_no_reconnect_path(
|
async def test_reconnect_fires_even_when_rotate_url_set(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""rotate_url задан → backconnect=False → одиночный 403 (max_rotations=0) raise сразу.
|
"""Прод-конфиг run-212 регрессия: scraper_proxy_url + leftover avito_proxy_rotate_url +
|
||||||
|
max_rotations=0 → reconnect-retry ВСЁ РАВНО срабатывает (gate только на наличие proxy).
|
||||||
|
|
||||||
Новая reconnect-ветка должна быть выключена когда есть changeip-URL: legacy
|
changeip-URL относится к ДРУГОМУ (auv/browser) прокси; curl_cffi egress через
|
||||||
single-IP mobile-proxy путь без изменений.
|
scraper_proxy_url, поэтому пересоздание сессии меняет exit-IP независимо от changeip.
|
||||||
|
С max_rotations=0 changeip-путь — no-op, fallback-reconnect снимает 403.
|
||||||
"""
|
"""
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
avito_mod.settings, "avito_proxy_rotate_url", "https://changeip.example/?proxy_key=x"
|
avito_mod.settings, "avito_proxy_rotate_url", "https://changeip.example/?proxy_key=x"
|
||||||
|
|
@ -154,18 +158,63 @@ async def test_non_backconnect_with_rotate_url_no_reconnect_path(
|
||||||
r403 = MagicMock()
|
r403 = MagicMock()
|
||||||
r403.status_code = 403
|
r403.status_code = 403
|
||||||
r403.text = ""
|
r403.text = ""
|
||||||
|
ok = MagicMock()
|
||||||
|
ok.status_code = 200
|
||||||
|
ok.text = _CARD_HTML
|
||||||
mock_session = AsyncMock()
|
mock_session = AsyncMock()
|
||||||
mock_session.get = AsyncMock(return_value=r403)
|
mock_session.get = AsyncMock(side_effect=[r403, r403, ok])
|
||||||
scraper._cffi = mock_session
|
scraper._cffi = mock_session
|
||||||
|
|
||||||
reset = AsyncMock()
|
reset = AsyncMock()
|
||||||
with (
|
with (
|
||||||
patch("app.services.scrapers.avito.asyncio.sleep", AsyncMock()),
|
patch("app.services.scrapers.avito.asyncio.sleep", AsyncMock()),
|
||||||
patch.object(AvitoScraper, "_reset_cffi", reset),
|
patch.object(AvitoScraper, "_reset_cffi", reset),
|
||||||
patch.object(AvitoScraper, "_rotate_ip", AsyncMock(return_value=True)),
|
patch.object(AvitoScraper, "_rotate_ip", AsyncMock(return_value=True)) as rot,
|
||||||
):
|
):
|
||||||
with pytest.raises(AvitoBlockedError):
|
html = await scraper._fetch_serp_html("https://www.avito.ru/x", page=1)
|
||||||
await scraper._fetch_serp_html("https://www.avito.ru/x", page=1)
|
|
||||||
|
|
||||||
reset.assert_not_awaited() # reconnect-ветка выключена при наличии rotate_url
|
assert html == _CARD_HTML # 403→403→200 снят reconnect'ом, нет raise
|
||||||
assert mock_session.get.await_count == 1 # немедленный raise
|
assert reset.await_count == 2 # reconnect сработал несмотря на rotate_url
|
||||||
|
rot.assert_not_awaited() # max_rotations=0 → changeip-путь не дёргается
|
||||||
|
assert mock_session.get.await_count == 3
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_changeip_path_tried_first_when_rotations_available(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""changeip precedence: scraper_proxy_url + rotate_url + max_rotations=1 → на 403
|
||||||
|
changeip-ротация пробуется ПЕРЕД reconnect-fallback.
|
||||||
|
|
||||||
|
Гарантирует, что новый gate не сломал приоритет legacy changeip-пути.
|
||||||
|
"""
|
||||||
|
monkeypatch.setattr(
|
||||||
|
avito_mod.settings, "avito_proxy_rotate_url", "https://changeip.example/?proxy_key=x"
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(avito_mod.settings, "avito_proxy_max_rotations", 1)
|
||||||
|
monkeypatch.setattr(avito_mod.settings, "scraper_proxy_url_env", "http://u:p@mproxy.site:14619")
|
||||||
|
|
||||||
|
scraper = AvitoScraper()
|
||||||
|
r403 = MagicMock()
|
||||||
|
r403.status_code = 403
|
||||||
|
r403.text = ""
|
||||||
|
ok = MagicMock()
|
||||||
|
ok.status_code = 200
|
||||||
|
ok.text = _CARD_HTML
|
||||||
|
mock_session = AsyncMock()
|
||||||
|
mock_session.get = AsyncMock(side_effect=[r403, ok])
|
||||||
|
scraper._cffi = mock_session
|
||||||
|
|
||||||
|
reset = AsyncMock()
|
||||||
|
rot = AsyncMock(return_value=True)
|
||||||
|
with (
|
||||||
|
patch("app.services.scrapers.avito.asyncio.sleep", AsyncMock()),
|
||||||
|
patch.object(AvitoScraper, "_reset_cffi", reset),
|
||||||
|
patch.object(AvitoScraper, "_rotate_ip", rot),
|
||||||
|
):
|
||||||
|
html = await scraper._fetch_serp_html("https://www.avito.ru/x", page=1)
|
||||||
|
|
||||||
|
assert html == _CARD_HTML
|
||||||
|
rot.assert_awaited_once() # changeip-путь использован первым на 403
|
||||||
|
reset.assert_not_awaited() # до reconnect-fallback не дошло
|
||||||
|
assert mock_session.get.await_count == 2
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue