tradein: HTTP POST /estimate отдаёт инвариантный центрально-схлопнутый результат и не персистит (estimate_quality напрямую — корректен) #1816

Closed
opened 2026-06-20 05:34:22 +00:00 by lekss361 · 1 comment
Owner

Симптом (live, 2026-06-20)

HTTP POST /api/v1/trade-in/estimate возвращает одинаковый центрально-схлопнутый результат для географически разных адресов и не сохраняет строку в trade_in_estimates. При этом тот же estimate_quality(payload, db), вызванный напрямую в контейнере tradein-backend, даёт корректный per-address результат.

Сравнение (один и тот же адрес-стринг, одна БД)

Адрес HTTP /estimate Прямой estimate_quality
Малышева 30, Екатеринбург (центр, 56.833) ppm2=208000, комплы «Малышева 35/Куйбышева 5/8 Марта 57» ppm2=207085, комплы Попова 18/Куйбышева 21/Радищева 33, n=10
Крестинского 44, Екатеринбург (SE, 56.788, ~5 км) ppm2=208000, комплы те же «Малышева 35/Куйбышева 5…» ppm2=152270, комплы Чкаловский/Ботанический (SE), n=4

HTTP отдаёт идентичные центральные комплы и ~208000 ₽/м² для обоих адресов (и для любой 2-комн); прямой вызов — корректные, разнесённые по районам.

Диагностика

  1. Не персистит. SELECT max(created_at) FROM trade_in_estimates = 2026-06-19 16:05:40 UTC (последний реальный вызов автора смоука). ~13 моих HTTP-вызовов (2026-06-19 20:40+ и далее) создали 0 строк — подтверждено и через SSH-туннель, и через docker exec tradein-postgres psql.
  2. Нет INFO-логов estimator'а во время HTTP-вызовов (docker logs tradein-backend), тогда как прямой вызов эмитит полный трейс analogs tier=… / anchor … / corridor clamp ….
  3. GET /estimate/{id} отдаёт данные для id, которых НЕТ в БД → ответ берётся из in-memory/кэша, маскируя отсутствие persist.
  4. Не кодировка. Тело — валидный UTF-8 (d09a d180=«Кр»), повтор с --data-binary + charset=utf-8 даёт тот же схлопнутый результат.
  5. Геокодер исправен. geocode("Крестинского 44, Екатеринбург", db) напрямую → lat=56.788 lon=60.625 prov=cache conf=exact (корректный SE), все целевые адреса резолвятся в разные верные точки.

Гипотезы (для разбора, root cause НЕ подтверждён)

  • HTTP-слой/прокси response-cache (Caddy?) кэширует POST /estimate по слишком грубому ключу → отдаёт один центральный результат всем.
  • Туннель localhost:8000 бьёт в устаревший/другой инстанс, не в свежий tradein-backend (контейнер рестартовал 2026-06-19 18:34 UTC; прямой вызов делался ВНУТРИ него и корректен).
  • Request-scoped кэш/路 в роуте, шунтирующий estimate_quality.

Репро

curl -X POST http://<backend>:8000/api/v1/trade-in/estimate \
  -H "Content-Type: application/json; charset=utf-8" -H "X-Authenticated-User: admin" \
  --data-binary '{"address":"Крестинского 44, Екатеринбург","area_m2":58,"rooms":2,"floor":5,"total_floors":9}'
# → median_price_per_m2 ≈ 208000, центральные комплы (НЕВЕРНО для SE-адреса)

vs прямой estimate_quality тем же payload → 152270, SE-комплы.

Влияние

Реальные пользователи ходят через HTTP → получают неверные (центрально-схлопнутые) оценки и оценки не пишутся (нет истории/аудита; GET работает только из кэша). Маскирует и корректность #1795 (фикс работает в estimate_quality, но HTTP это не показывает).

DoD

  • HTTP /estimate для разнесённых адресов даёт разные, per-address корректные результаты (= прямой estimate_quality).
  • Каждый успешный HTTP-вызов пишет строку в trade_in_estimates (GET /estimate/{id} читает из БД, не из кэша для несуществующих id).
  • Определить и убрать источник схлопывания (прокси-кэш / роутинг туннеля / request-cache).

Связано: #1795 (верифицирован прямым вызовом; этот баг блокировал HTTP-смоук), #1786 (геокод зданий — исправен).

## Симптом (live, 2026-06-20) HTTP `POST /api/v1/trade-in/estimate` возвращает **одинаковый центрально-схлопнутый результат для географически разных адресов** и **не сохраняет строку** в `trade_in_estimates`. При этом тот же `estimate_quality(payload, db)`, вызванный **напрямую** в контейнере `tradein-backend`, даёт корректный per-address результат. ### Сравнение (один и тот же адрес-стринг, одна БД) | Адрес | HTTP `/estimate` | Прямой `estimate_quality` | |---|---|---| | Малышева 30, Екатеринбург (центр, 56.833) | ppm2=**208000**, комплы «Малышева 35/Куйбышева 5/8 Марта 57» | ppm2=**207085**, комплы Попова 18/Куйбышева 21/Радищева 33, n=10 | | Крестинского 44, Екатеринбург (SE, 56.788, ~5 км) | ppm2=**208000**, комплы **те же** «Малышева 35/Куйбышева 5…» | ppm2=**152270**, комплы **Чкаловский/Ботанический (SE)**, n=4 | HTTP отдаёт идентичные центральные комплы и ~208000 ₽/м² для обоих адресов (и для любой 2-комн); прямой вызов — корректные, разнесённые по районам. ## Диагностика 1. **Не персистит.** `SELECT max(created_at) FROM trade_in_estimates` = `2026-06-19 16:05:40 UTC` (последний реальный вызов автора смоука). ~13 моих HTTP-вызовов (2026-06-19 20:40+ и далее) создали **0 строк** — подтверждено и через SSH-туннель, и через `docker exec tradein-postgres psql`. 2. **Нет INFO-логов estimator'а** во время HTTP-вызовов (`docker logs tradein-backend`), тогда как прямой вызов эмитит полный трейс `analogs tier=… / anchor … / corridor clamp …`. 3. **GET `/estimate/{id}` отдаёт данные для id, которых НЕТ в БД** → ответ берётся из in-memory/кэша, маскируя отсутствие persist. 4. **Не кодировка.** Тело — валидный UTF-8 (`d09a d180`=«Кр»), повтор с `--data-binary` + `charset=utf-8` даёт тот же схлопнутый результат. 5. **Геокодер исправен.** `geocode("Крестинского 44, Екатеринбург", db)` напрямую → `lat=56.788 lon=60.625 prov=cache conf=exact` (корректный SE), все целевые адреса резолвятся в разные верные точки. ## Гипотезы (для разбора, root cause НЕ подтверждён) - **HTTP-слой/прокси response-cache** (Caddy?) кэширует POST `/estimate` по слишком грубому ключу → отдаёт один центральный результат всем. - **Туннель `localhost:8000` бьёт в устаревший/другой инстанс**, не в свежий `tradein-backend` (контейнер рестартовал 2026-06-19 18:34 UTC; прямой вызов делался ВНУТРИ него и корректен). - Request-scoped кэш/路 в роуте, шунтирующий `estimate_quality`. ## Репро ``` curl -X POST http://<backend>:8000/api/v1/trade-in/estimate \ -H "Content-Type: application/json; charset=utf-8" -H "X-Authenticated-User: admin" \ --data-binary '{"address":"Крестинского 44, Екатеринбург","area_m2":58,"rooms":2,"floor":5,"total_floors":9}' # → median_price_per_m2 ≈ 208000, центральные комплы (НЕВЕРНО для SE-адреса) ``` vs прямой `estimate_quality` тем же payload → 152270, SE-комплы. ## Влияние Реальные пользователи ходят через HTTP → получают **неверные (центрально-схлопнутые) оценки** и оценки **не пишутся** (нет истории/аудита; GET работает только из кэша). Маскирует и корректность #1795 (фикс работает в `estimate_quality`, но HTTP это не показывает). ## DoD - HTTP `/estimate` для разнесённых адресов даёт разные, per-address корректные результаты (= прямой `estimate_quality`). - Каждый успешный HTTP-вызов пишет строку в `trade_in_estimates` (GET `/estimate/{id}` читает из БД, не из кэша для несуществующих id). - Определить и убрать источник схлопывания (прокси-кэш / роутинг туннеля / request-cache). Связано: #1795 (верифицирован прямым вызовом; этот баг блокировал HTTP-смоук), #1786 (геокод зданий — исправен).
lekss361 added the
bug
priority/p1
scope/backend
tradein
labels 2026-06-20 05:50:35 +00:00
Author
Owner

НЕ БАГ — артефакт тестового туннеля (root cause найден, закрываю)

Симптом объясняется тем, что HTTP-смоук бил не в тот бэкенд.

Root cause: SSH LocalForward 8000 → 127.0.0.1:8000 на прод-хосте указывает на контейнер gendesign-backend-1 (Site Finder / GenDesign бэкенд, db=gendesign, публикует 127.0.0.1:8000->8000, up 41ч — старый код до #1795). Это НЕ trade-in бэкенд.

  • tradein-backend порт 8000 не опубликован на хост (только docker-net), реальные юзеры идут через Caddy: reverse_proxy tradein-backend:8000.
  • Значит весь HTTP-смоук через туннель:8000 шёл в gendesign-backend-1 (чужой код + gendesign-БД) → отсюда инвариантные 208000, центр-коллапс и отсутствие записей в trade_in_estimates (он писал в свою БД).

Проверка реального пути: /estimate изнутри контейнера tradein-backend (docker exec ... curl localhost:8000) для «Крестинского 44, Екатеринбург» → 152270, conf=medium, n=4, SE-комплы Чкаловский/Ботанический — корректно, per-address. Это тот же путь, что у реальных юзеров (Caddy→tradein-backend).

Вывод: прод-бага нет, оценка работает корректно (включая #1795). Единственное реальное наблюдение — gendesign-backend-1 тоже отвечает на /api/v1/trade-in/* старым кодом, но доступен только по 127.0.0.1 хоста (не публично), на юзеров не влияет — housekeeping, не блокер.

Закрываю как invalid.

## ❌ НЕ БАГ — артефакт тестового туннеля (root cause найден, закрываю) Симптом объясняется тем, что HTTP-смоук бил **не в тот бэкенд**. **Root cause:** SSH `LocalForward 8000 → 127.0.0.1:8000` на прод-хосте указывает на контейнер **`gendesign-backend-1`** (Site Finder / GenDesign бэкенд, `db=gendesign`, публикует `127.0.0.1:8000->8000`, up 41ч — старый код до #1795). Это **НЕ** trade-in бэкенд. - `tradein-backend` порт 8000 **не опубликован** на хост (только docker-net), реальные юзеры идут через Caddy: `reverse_proxy tradein-backend:8000`. - Значит весь HTTP-смоук через туннель:8000 шёл в gendesign-backend-1 (чужой код + gendesign-БД) → отсюда инвариантные 208000, центр-коллапс и отсутствие записей в `trade_in_estimates` (он писал в свою БД). **Проверка реального пути:** `/estimate` **изнутри контейнера `tradein-backend`** (`docker exec ... curl localhost:8000`) для «Крестинского 44, Екатеринбург» → **152270, conf=medium, n=4, SE-комплы Чкаловский/Ботанический** — корректно, per-address. Это тот же путь, что у реальных юзеров (Caddy→tradein-backend). **Вывод:** прод-бага нет, оценка работает корректно (включая #1795). Единственное реальное наблюдение — `gendesign-backend-1` тоже отвечает на `/api/v1/trade-in/*` старым кодом, но доступен только по `127.0.0.1` хоста (не публично), на юзеров не влияет — housekeeping, не блокер. Закрываю как invalid.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#1816
No description provided.