From d9d6caa04406fec737b6d2f9980a4f5c4712c976 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sun, 31 May 2026 11:53:51 +0300 Subject: [PATCH] =?UTF-8?q?fix(tradein):=20single-run=20guard=20=D0=BD?= =?UTF-8?q?=D0=B0=20avito-city-sweep=20admin-endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Параллельные avito-sweep'ы делят один прокси-IP → удвоенный rate → Avito бан. Incident 2026-05-31: runs #26 (08:31-08:42) и #27 (08:38-08:43) шли одновременно → оба banned (скомпрометировало delay-эксперимент). Scheduler уже защищён (has_running_run + advisory lock в _claim_run), но ручной admin-endpoint POST /scrape/avito-city-sweep guard'а не имел. - admin.py: has_running_run('avito_city_sweep') → HTTPException 409 перед create_run. - test: monkeypatch has_running_run→True → 409. --- tradein-mvp/backend/app/api/v1/admin.py | 16 ++++++++++++++++ .../backend/tests/test_api_avito_stage4a.py | 18 ++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/tradein-mvp/backend/app/api/v1/admin.py b/tradein-mvp/backend/app/api/v1/admin.py index e771642c..22f41677 100644 --- a/tradein-mvp/backend/app/api/v1/admin.py +++ b/tradein-mvp/backend/app/api/v1/admin.py @@ -22,6 +22,7 @@ from app.schemas.trade_in import ScheduleConfig, ScheduleConfigUpdate from app.services import cian_session as cian_session_svc from app.services import scrape_runs as runs_mod from app.services.geocoder import geocode +from app.services.scheduler import has_running_run from app.services.scrape_pipeline import ( run_avito_city_sweep, run_cian_city_sweep, @@ -634,7 +635,22 @@ async def start_avito_city_sweep( Coop cancel: POST /scrape/avito-city-sweep/{run_id}/cancel помечает run, pipeline проверяет статус каждый anchor. + + Single-run guard: второй sweep запустить нельзя, пока первый running — + параллельные раны делят один прокси-IP → удвоенный rate → Avito бан + (incident 2026-05-31: runs #26+#27 шли одновременно → оба banned). + Scheduler уже защищён (has_running_run + advisory lock); этот guard + закрывает ручной admin-триггер. """ + if has_running_run(db, "avito_city_sweep"): + raise HTTPException( + status_code=409, + detail=( + "avito city-sweep уже выполняется — одновременно допустим только один " + "(параллельные раны делят прокси-IP → Avito бан). Дождитесь завершения " + "или отмените текущий через POST /scrape/avito-city-sweep/{run_id}/cancel." + ), + ) run_id = runs_mod.create_run(db, source="avito_city_sweep", params=payload.model_dump()) async def _sweep_task() -> None: diff --git a/tradein-mvp/backend/tests/test_api_avito_stage4a.py b/tradein-mvp/backend/tests/test_api_avito_stage4a.py index b4424a8d..e20df933 100644 --- a/tradein-mvp/backend/tests/test_api_avito_stage4a.py +++ b/tradein-mvp/backend/tests/test_api_avito_stage4a.py @@ -104,3 +104,21 @@ def test_get_imv_benchmark_unavailable(app): # endpoint вернёт 404 потому что fallback тоже не находит address → нужен estimate row # Этот тест проверяет что эндпоинт mounted; 404 ОК assert r.status_code in (200, 404) + + +def test_avito_city_sweep_single_run_guard(app, monkeypatch): + """#single-run: второй avito sweep при уже running → 409. + + Параллельные sweep'ы делят один прокси-IP → удвоенный rate → Avito бан + (incident 2026-05-31: runs #26+#27 одновременно → оба banned). + """ + from app.api.v1 import admin as admin_module + + monkeypatch.setattr(admin_module, "has_running_run", lambda db, source: True) + client = TestClient(app) + r = client.post( + "/api/v1/admin/scrape/avito-city-sweep", + json={"pages_per_anchor": 1, "request_delay_sec": 5.0}, + ) + assert r.status_code == 409 + assert "уже выполняется" in r.json()["detail"]