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"]