PR #3405 писал scrape_runs.proxy_id только из proxy_pool.acquire(). Прогоны, которые берут прокси через proxy_egress.resolve_proxy_url без аренды (yandex_detail_backfill, yandex_address_backfill, curl-ветка avito_detail_backfill), атрибуцию не получали: прод 17.09 — yandex_detail_backfill 0 из 56, yandex_address_backfill 0 из 1. resolve_proxy_url при выбранном узле и выставленном current_run_id зовёт attribute_run_proxy. Своей короткой сессией: db вызывающего — долгоживущая сессия прогона посреди работы, а атрибуция коммитит и на сбое откатывает. Тесты на живом Postgres: атрибуция внутри прогона (proxy_id и counters.proxy_ids), вне прогона scrape_runs не тронут, незакоммиченная работа вызывающего не коммитится. Красные прогоны: на старом коде «assert None == 70»; с атрибуцией через db вызывающего — «атрибуция закоммитила чужую транзакцию». Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
148 lines
5.6 KiB
Python
148 lines
5.6 KiB
Python
"""Прогон на egress-пути знает свой узел (#3404, хвост PR #3405).
|
||
|
||
PR #3405 писал `scrape_runs.proxy_id` только из `proxy_pool.acquire()`. Прогоны, которые
|
||
берут прокси через `proxy_egress.resolve_proxy_url` (без аренды: yandex_detail_backfill,
|
||
yandex_address_backfill, curl-ветка avito_detail_backfill), атрибуцию не получали —
|
||
прод 17.09: у yandex_detail_backfill proxy_id NULL в 56 прогонах из 56.
|
||
|
||
Живой Postgres, настоящие сессии (атрибуция идёт своей сессией, поэтому строки
|
||
коммитятся и удаляются в finally). Без БД — skip.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
|
||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
|
||
|
||
import json
|
||
from collections.abc import Iterator
|
||
from typing import Any
|
||
|
||
import pytest
|
||
from scraper_kit.orchestration.run_context import current_run_id
|
||
from sqlalchemy import create_engine, text
|
||
from sqlalchemy.orm import Session, sessionmaker
|
||
|
||
from app.services import proxy_egress
|
||
|
||
|
||
def _live_engine() -> Any | None:
|
||
dsn = os.environ.get("TEST_DATABASE_URL") or os.environ.get("DATABASE_URL", "")
|
||
if not dsn or "localhost:5432/test" in dsn:
|
||
return None
|
||
try:
|
||
engine = create_engine(dsn, future=True)
|
||
with engine.connect() as conn:
|
||
conn.execute(text("SELECT proxy_id FROM scrape_runs LIMIT 1"))
|
||
return engine
|
||
except Exception:
|
||
return None
|
||
|
||
|
||
_ENGINE = _live_engine()
|
||
pytestmark = pytest.mark.skipif(_ENGINE is None, reason="no reachable Postgres test DB")
|
||
|
||
|
||
@pytest.fixture
|
||
def env(monkeypatch: pytest.MonkeyPatch) -> Iterator[dict[str, Any]]:
|
||
"""Узел пула + два прогона: `run` — текущий, `other` — строка, которую вызывающий
|
||
держит изменённой без коммита."""
|
||
assert _ENGINE is not None
|
||
factory = sessionmaker(bind=_ENGINE, future=True)
|
||
monkeypatch.setattr(proxy_egress, "_SessionLocal", factory)
|
||
setup = factory()
|
||
proxy_id = setup.execute(
|
||
text(
|
||
"INSERT INTO scrape_proxies (url, provider_affinity) "
|
||
"VALUES ('http://t3404-' || gen_random_uuid(), 'any') RETURNING id"
|
||
)
|
||
).scalar_one()
|
||
run_ids = [
|
||
setup.execute(
|
||
text(
|
||
"INSERT INTO scrape_runs (source, status) "
|
||
"VALUES ('test_3404', 'running') RETURNING id"
|
||
)
|
||
).scalar_one()
|
||
for _ in range(2)
|
||
]
|
||
setup.commit()
|
||
token = current_run_id.set(None)
|
||
try:
|
||
yield {"proxy_id": int(proxy_id), "run": int(run_ids[0]), "other": int(run_ids[1])}
|
||
finally:
|
||
current_run_id.reset(token)
|
||
setup.rollback()
|
||
setup.execute(text("DELETE FROM scrape_runs WHERE id = ANY(:ids)"), {"ids": run_ids})
|
||
setup.execute(text("DELETE FROM scrape_proxies WHERE id = :id"), {"id": proxy_id})
|
||
setup.commit()
|
||
setup.close()
|
||
|
||
|
||
def _run_row(run_id: int) -> Any:
|
||
assert _ENGINE is not None
|
||
with _ENGINE.connect() as conn:
|
||
return conn.execute(
|
||
text("SELECT proxy_id, counters FROM scrape_runs WHERE id = :id"), {"id": run_id}
|
||
).one()
|
||
|
||
|
||
def _node_id_of(url: str) -> int:
|
||
assert _ENGINE is not None
|
||
with _ENGINE.connect() as conn:
|
||
return int(
|
||
conn.execute(
|
||
text("SELECT id FROM scrape_proxies WHERE url = :u"), {"u": url}
|
||
).scalar_one()
|
||
)
|
||
|
||
|
||
def test_resolve_within_run_writes_node_to_scrape_runs(env: dict[str, Any]) -> None:
|
||
"""Главный случай: прогон идёт, egress выбран — прогон знает узел.
|
||
|
||
На старом коде proxy_id остаётся NULL."""
|
||
current_run_id.set(env["run"])
|
||
caller = Session(bind=_ENGINE, future=True)
|
||
try:
|
||
url = proxy_egress.resolve_proxy_url(caller, "yandex")
|
||
finally:
|
||
caller.close()
|
||
|
||
assert url is not None
|
||
node = _node_id_of(url)
|
||
row = _run_row(env["run"])
|
||
assert row.proxy_id == node
|
||
counters = row.counters if isinstance(row.counters, dict) else json.loads(row.counters)
|
||
assert counters.get("proxy_ids") == [node]
|
||
|
||
|
||
def test_resolve_outside_run_writes_nothing(env: dict[str, Any]) -> None:
|
||
"""Без прогона (админка, проверка кук) — scrape_runs не трогаем."""
|
||
caller = Session(bind=_ENGINE, future=True)
|
||
try:
|
||
assert proxy_egress.resolve_proxy_url(caller, "yandex") is not None
|
||
finally:
|
||
caller.close()
|
||
|
||
assert _run_row(env["run"]).proxy_id is None
|
||
|
||
|
||
def test_attribution_does_not_commit_callers_transaction(env: dict[str, Any]) -> None:
|
||
"""db вызывающего — долгоживущая сессия прогона посреди работы. Атрибуция не имеет
|
||
права закоммитить его незавершённые изменения (или откатить их на сбое)."""
|
||
current_run_id.set(env["run"])
|
||
caller = Session(bind=_ENGINE, future=True)
|
||
try:
|
||
caller.execute(
|
||
text("UPDATE scrape_runs SET counters = '{\"t3404\": 1}'::jsonb WHERE id = :id"),
|
||
{"id": env["other"]},
|
||
)
|
||
proxy_egress.resolve_proxy_url(caller, "yandex")
|
||
caller.rollback()
|
||
finally:
|
||
caller.close()
|
||
|
||
other = _run_row(env["other"]).counters or {}
|
||
assert "t3404" not in other, "атрибуция закоммитила чужую транзакцию"
|
||
assert _run_row(env["run"]).proxy_id is not None
|