From de5f4a32cd9941cd29f9dbb27f2888df895571af Mon Sep 17 00:00:00 2001 From: bot-backend Date: Fri, 11 Sep 2026 18:51:38 +0300 Subject: [PATCH] =?UTF-8?q?fix(msk):=20=D1=81=D1=83=D1=85=D0=BE=D0=B9=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B3=D0=BE=D0=BD=20=D0=BF=D1=80=D0=B5=D0=B4?= =?UTF-8?q?-=D0=B3=D0=B5=D0=BE=D0=BA=D0=BE=D0=B4=D0=B0=20=D0=BF=D0=B0?= =?UTF-8?q?=D0=B4=D0=B0=D0=BB=20=D0=BD=D0=B0=20=D0=BE=D1=82=D1=81=D1=83?= =?UTF-8?q?=D1=82=D1=81=D1=82=D0=B2=D1=83=D1=8E=D1=89=D0=B5=D0=B9=20=D0=BA?= =?UTF-8?q?=D1=8D=D1=88-=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Кэш `msk_raw.avito_geocode` создаётся только боевым прогоном (`geocode and not dry_run`), а читается безусловно. На проде это роняло `--dry-run` первым же запросом — UndefinedTable msk_raw.avito_geocode, то есть ломалась ровно та репетиция, ради которой сухой прогон и существует. Наличие отношения проверяется через `to_regclass`, а не ловится исключением: в Postgres упавший оператор кладёт транзакцию целиком, и except потребовал бы rollback посреди чужого батча. Замер после правки (500 карточек, прод): отобрано 458, область 7, не разрешено 35 (7%), геокод-вызовов 338 на 500 карточек — дедупликация адреса внутри страницы работает. Счётчики сходятся. Заодно выяснилось, что дневная квота DaData на подсказки — 200 000, а не 10 000: `stat/daily` на проде показывает suggestions remaining 200000 при нулевом расходе. Весь корпус (21 565 различных адресов) проходит за один заход, дробить на трое суток не нужно. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01VQ8jqr4SFirX5tFLwdSrXh --- .../backend/app/tasks/msk_raw_import.py | 13 ++++++- .../backend/tests/test_msk_raw_import.py | 39 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/tradein-mvp/backend/app/tasks/msk_raw_import.py b/tradein-mvp/backend/app/tasks/msk_raw_import.py index a4d337ca..5382c0d6 100644 --- a/tradein-mvp/backend/app/tasks/msk_raw_import.py +++ b/tradein-mvp/backend/app/tasks/msk_raw_import.py @@ -224,6 +224,7 @@ CREATE TABLE IF NOT EXISTS msk_raw.avito_geocode ( resolved_at timestamptz NOT NULL DEFAULT NOW() ) """ +_GEO_CACHE_EXISTS = "SELECT to_regclass('msk_raw.avito_geocode')" _GEO_CACHE_SELECT = """ SELECT cache_key, lat, lon, region_code, full_address FROM msk_raw.avito_geocode @@ -350,7 +351,17 @@ def _resolve_points( return {} points: dict[str, GeoPoint | None] = {} - cached = db.execute(text(_GEO_CACHE_SELECT), {"keys": list(wanted)}).mappings().all() + # Кэш создаётся только боевым прогоном (`geocode and not dry_run`), поэтому на + # первом `--dry-run` таблицы ещё нет и SELECT по ней роняет весь замер — то + # есть ломается ровно та репетиция, ради которой сухой прогон и существует. + # Проверяем наличие отношения, а не ловим исключение: в Postgres упавший + # оператор кладёт транзакцию целиком, и except потребовал бы rollback. + cache_exists = db.execute(text(_GEO_CACHE_EXISTS)).scalar() is not None + cached = ( + db.execute(text(_GEO_CACHE_SELECT), {"keys": list(wanted)}).mappings().all() + if cache_exists + else () + ) for row in cached: points[row["cache_key"]] = ( None diff --git a/tradein-mvp/backend/tests/test_msk_raw_import.py b/tradein-mvp/backend/tests/test_msk_raw_import.py index 9f44dff8..4ed0a2d7 100644 --- a/tradein-mvp/backend/tests/test_msk_raw_import.py +++ b/tradein-mvp/backend/tests/test_msk_raw_import.py @@ -105,6 +105,10 @@ class _FakeResult: def all(self) -> list[dict[str, Any]]: return self._rows + def scalar(self) -> Any: + """Проба существования кэш-таблицы: None = таблицы ещё нет.""" + return self._rows[0] if self._rows else None + class _FakeSession: """Отдаёт строки постранично по тому же keyset-условию, что и настоящая вью.""" @@ -361,3 +365,38 @@ def test_geocode_is_refused_for_cian(captured: list[dict[str, Any]]) -> None: """У Циана город виден по префиксу округа даром — квоту на него не тратим.""" with pytest.raises(SystemExit): mod.import_msk_raw(_FakeSession(_rows()), source="cian", batch_size=10, geocode=True) + + +def test_dry_run_survives_missing_geocode_cache(monkeypatch: pytest.MonkeyPatch) -> None: + """Первый `--dry-run` идёт по проду, где кэш-таблицы ещё нет. + + Таблицу создаёт только боевой прогон, поэтому SELECT по ней в сухом ронял + ровно ту репетицию, ради которой сухой прогон и существует (замер на проде + 11.09: UndefinedTable msk_raw.avito_geocode). Резолвер обязан молча считать + кэш пустым и идти к DaData за остатком. + """ + + class _NoCacheSession: + def __init__(self) -> None: + self.cache_reads = 0 + + def execute(self, sql: Any, params: dict[str, Any] | None = None) -> _FakeResult: + if "to_regclass" in str(sql): + return _FakeResult([]) # таблицы нет + self.cache_reads += 1 + raise AssertionError("к отсутствующей кэш-таблице ходить нельзя") + + point = mod.GeoPoint(55.6412, 37.6089, 77, "г Москва, ш Варшавское, д 62 к 1") + + async def _fake_geocode(misses: list[Any]) -> dict[str, Any]: + return {misses[0][0]: point} + + monkeypatch.setattr(mod, "_geocode_many", _fake_geocode) + + db = _NoCacheSession() + points = mod._resolve_points( + db, [{"id": 1, "payload": _avito()}], mod.GeocodeBudget(10), dry_run=True + ) + + assert db.cache_reads == 0 + assert list(points.values()) == [point]