[SF-v2 #950] 950-E5: Cross-load ETL tradein→gendesign newbuilding #976

Closed
opened 2026-06-02 19:21:59 +00:00 by lekss361 · 4 comments
Owner

Часть EPIC #950 (Site Finder v2 / GG-форсайт).

РЕШЕНИЕ ПРИНЯТО (Anton, 2026-06-12): вариант A — прямой psycopg-ETL

Из анализа cross-DB путей (коммент 2026-06-07): выбран A) прямой psycopg-ETL через общую docker-сеть. B (postgres_fdw) отклонён. Стратегический контекст: канал делается с прицелом на будущий «market-слой» (общий слой рыночных данных, который читают оба продукта) — ADR в vault decisions/. Связано: #1182 (вынос scheduler в отдельный контейнер).

Предыстория (ground truth 2026-06-07)

  • E4 (#975): таблица newbuilding_listings в gendesign — готова, merged (#1108), uq (source, ext_house_id).
  • Канала gendesign→tradein нет: FDW существует только в обратную сторону (м.100, tradein читает gendesign); gendesign-backend в сети gendesign_default, tradein-postgres в gendesign_shared+tradein-net — общей сети нет; в gendesign/backend/.env нет tradein-кредов.

План (по шагам, в одном PR кроме шага 3)

  1. Сеть: docker-compose.prod.yml (gendesign) — подключить gendesign-backend (+ celery worker, если ETL пойдёт через Celery task) к external network gendesign_shared.
  2. Роль: read-only роль gendesign_reader на tradein-postgres — SELECT на houses, houses_price_dynamics, house_reliability_checks, house_reviews, house_sources. Новая tradein-mvp/data/sql/NN_*.sql миграция. ⚠️ tradein-миграции авто-применяются на прод при деплое (_schema_migrations в deploy-tradein.yml) — DDL писать идемпотентно, пароль НЕ хардкодить (через \set/env-bootstrap по паттерну м.100).
  3. Креды (needs-human шаг): пароль роли → Forgejo secret → TRADEIN_DATABASE_URL в gendesign backend/.env через deploy workflow. Worker оформляет wiring в deploy.yml; само значение секрета добавляет человек/resolver в Forgejo UI.
  4. ETL: backend/app/services/etl/newbuilding_crossload.pySELECT houses(+enrichment) из tradein → идемпотентный upsert в newbuilding_listings по (source, ext_house_id); admin endpoint + расписание.

DoD

  • идемпотентный upsert; повторный прогон не меняет счётчики
  • счётчики source (tradein houses) vs target (newbuilding_listings) совпадают; verify-запросы в PR
  • запланирован (nightly), залогирован
  • разблокирует #999 (4-й слой карты «будущие ЖК» — после API-экспозиции newbuilding в analyze)

Зависит от: 950-E4 (#1108), 950-E1
Effort: L

**Часть EPIC #950 (Site Finder v2 / GG-форсайт).** ## ✅ РЕШЕНИЕ ПРИНЯТО (Anton, 2026-06-12): вариант A — прямой psycopg-ETL Из анализа cross-DB путей (коммент 2026-06-07): выбран **A) прямой psycopg-ETL** через общую docker-сеть. B (postgres_fdw) отклонён. Стратегический контекст: канал делается с прицелом на будущий **«market-слой»** (общий слой рыночных данных, который читают оба продукта) — ADR в vault `decisions/`. Связано: #1182 (вынос scheduler в отдельный контейнер). ## Предыстория (ground truth 2026-06-07) - E4 (#975): таблица `newbuilding_listings` в gendesign — **готова, merged (#1108)**, uq `(source, ext_house_id)`. - Канала gendesign→tradein нет: FDW существует только в обратную сторону (м.100, tradein читает gendesign); `gendesign-backend` в сети `gendesign_default`, `tradein-postgres` в `gendesign_shared`+`tradein-net` — общей сети нет; в `gendesign/backend/.env` нет tradein-кредов. ## План (по шагам, в одном PR кроме шага 3) 1. **Сеть:** `docker-compose.prod.yml` (gendesign) — подключить `gendesign-backend` (+ celery worker, если ETL пойдёт через Celery task) к external network `gendesign_shared`. 2. **Роль:** read-only роль `gendesign_reader` на tradein-postgres — `SELECT` на `houses`, `houses_price_dynamics`, `house_reliability_checks`, `house_reviews`, `house_sources`. Новая `tradein-mvp/data/sql/NN_*.sql` миграция. ⚠️ tradein-миграции **авто-применяются на прод при деплое** (`_schema_migrations` в deploy-tradein.yml) — DDL писать идемпотентно, пароль НЕ хардкодить (через `\set`/env-bootstrap по паттерну м.100). 3. **Креды (needs-human шаг):** пароль роли → Forgejo secret → `TRADEIN_DATABASE_URL` в gendesign `backend/.env` через deploy workflow. Worker оформляет wiring в deploy.yml; само значение секрета добавляет человек/resolver в Forgejo UI. 4. **ETL:** `backend/app/services/etl/newbuilding_crossload.py` — `SELECT` houses(+enrichment) из tradein → идемпотентный upsert в `newbuilding_listings` по `(source, ext_house_id)`; admin endpoint + расписание. ## DoD - идемпотентный upsert; повторный прогон не меняет счётчики - счётчики source (tradein houses) vs target (newbuilding_listings) совпадают; verify-запросы в PR - запланирован (nightly), залогирован - разблокирует #999 (4-й слой карты «будущие ЖК» — после API-экспозиции newbuilding в analyze) **Зависит от:** 950-E4 ✅ (#1108), 950-E1 **Effort:** L
lekss361 added this to the Site Finder v2 — Foresight (GG-форсайт) milestone 2026-06-02 19:22:03 +00:00
lekss361 added the
GG-форсайт
scope/backend
site-finder
labels 2026-06-02 19:22:03 +00:00
Author
Owner

Глубокий анализ cross-DB пути (прод, 2026-06-07) — ETL заблокирован инфра-prereq

E4 (#975, таблица newbuilding_listings) — готова, merged (#1108). Но перенос tradein→gendesign упирается в отсутствующий cross-DB канал:

  1. FDW не в ту сторону. Существующий tradein_fdw_reader (м.100) — это роль, которой tradein читает gendesign (v_tradein_cad_buildings). Обратного (gendesign читает tradein) НЕТ: на gendesign-postgres pg_foreign_server=0, foreign_tables=0.
  2. Сетевая изоляция. gendesign-backend в сети gendesign_default; tradein-postgres в gendesign_shared+tradein-net. Общей сети нет → gendesign-backend физически не достучится до tradein-postgres.
  3. Нет кредов. В gendesign/backend/.env нет TRADEIN DB-conn.

Рекомендация (нужно решение по подходу — needs-human/devops)

Два пути, оба требуют инфра-шага:

  • A) Прямой psycopg-ETL (проще для кода): добавить gendesign-backend+worker в сеть gendesign_shared (docker-compose.prod.yml) + reader-роль на tradein-postgres + TRADEIN_DATABASE_URL в backend/.env. Затем newbuilding_crossload.py коннектится к tradein, SELECT houses → upsert newbuilding_listings.
  • B) postgres_fdw gendesign→tradein (зеркало м.100 наоборот): foreign server + user mapping + foreign table houses + пароль-bootstrap в deploy.yml. ETL = INSERT…SELECT из foreign table.

Код-сторона ETL (idempotent upsert по uq_newbuilding_listings (source, ext_house_id), маппинг колонок houses→newbuilding_listings) готов к написанию сразу после выбора A/B + провижна сети/кредов. Сам провижн (прод-сеть + секрет) — devops/needs-human, не беру автономно.

→ Флипаю status/readyneeds-human: выбрать A/B + провижн cross-DB канала. После этого беру ETL-код.

## Глубокий анализ cross-DB пути (прод, 2026-06-07) — ETL заблокирован инфра-prereq E4 (#975, таблица `newbuilding_listings`) — **готова, merged** (#1108). Но перенос tradein→gendesign упирается в отсутствующий cross-DB канал: 1. **FDW не в ту сторону.** Существующий `tradein_fdw_reader` (м.100) — это роль, которой **tradein читает gendesign** (`v_tradein_cad_buildings`). Обратного (gendesign читает tradein) НЕТ: на gendesign-postgres `pg_foreign_server=0`, `foreign_tables=0`. 2. **Сетевая изоляция.** `gendesign-backend` в сети `gendesign_default`; `tradein-postgres` в `gendesign_shared`+`tradein-net`. **Общей сети нет** → gendesign-backend физически не достучится до tradein-postgres. 3. **Нет кредов.** В `gendesign/backend/.env` нет TRADEIN DB-conn. ### Рекомендация (нужно решение по подходу — `needs-human`/devops) Два пути, оба требуют инфра-шага: - **A) Прямой psycopg-ETL** (проще для кода): добавить `gendesign-backend`+`worker` в сеть `gendesign_shared` (docker-compose.prod.yml) + reader-роль на tradein-postgres + `TRADEIN_DATABASE_URL` в `backend/.env`. Затем `newbuilding_crossload.py` коннектится к tradein, `SELECT houses` → upsert `newbuilding_listings`. - **B) postgres_fdw gendesign→tradein** (зеркало м.100 наоборот): foreign server + user mapping + foreign table `houses` + пароль-bootstrap в deploy.yml. ETL = `INSERT…SELECT` из foreign table. Код-сторона ETL (idempotent upsert по `uq_newbuilding_listings (source, ext_house_id)`, маппинг колонок houses→newbuilding_listings) готов к написанию сразу после выбора A/B + провижна сети/кредов. Сам провижн (прод-сеть + секрет) — devops/`needs-human`, не беру автономно. → Флипаю `status/ready`→`needs-human`: выбрать A/B + провижн cross-DB канала. После этого беру ETL-код.
lekss361 added the
needs-human
label 2026-06-06 21:51:58 +00:00
lekss361 added
status/ready
and removed
needs-human
labels 2026-06-12 07:37:03 +00:00
lekss361 added
status/wip
and removed
status/ready
labels 2026-06-12 08:02:34 +00:00
lekss361 added
status/review
and removed
status/wip
labels 2026-06-12 08:27:40 +00:00
Author
Owner

Working on this in PR #1185 (feat/sf-newbuilding-crossload, head 04da30b).

Все 4 шага плана в одном PR; pre-push review поймал 🔴 (psql -c :'var' не интерполируется — повтор инцидента 2026-05-24) → исправлено на stdin-паттерн ops/db-bootstrap/set_gendesign_reader_password.sql до пуша.

После merge нужны ручные шаги (Anton/resolver):

  1. tradein .env.runtime: TRADEIN_READER_PASSWORD=<пароль> (следующий tradein-деплой выставит его роли).
  2. gendesign backend/.env.runtime: TRADEIN_DATABASE_URL=postgresql://gendesign_reader:<тот же пароль>@tradein-postgres:5432/tradein + docker compose up -d --force-recreate --no-deps backend worker.
    До этого ETL в disabled-режиме (без ошибок), admin endpoint вернёт 503.
Working on this in PR #1185 (`feat/sf-newbuilding-crossload`, head 04da30b). Все 4 шага плана в одном PR; pre-push review поймал 🔴 (psql `-c :'var'` не интерполируется — повтор инцидента 2026-05-24) → исправлено на stdin-паттерн `ops/db-bootstrap/set_gendesign_reader_password.sql` до пуша. **После merge нужны ручные шаги (Anton/resolver):** 1. tradein `.env.runtime`: `TRADEIN_READER_PASSWORD=<пароль>` (следующий tradein-деплой выставит его роли). 2. gendesign `backend/.env.runtime`: `TRADEIN_DATABASE_URL=postgresql://gendesign_reader:<тот же пароль>@tradein-postgres:5432/tradein` + `docker compose up -d --force-recreate --no-deps backend worker`. До этого ETL в disabled-режиме (без ошибок), admin endpoint вернёт 503.
Author
Owner

Ручные шаги из предыдущего коммента уже выполнены (2026-06-12, по указанию Anton, через ssh):

  • TRADEIN_READER_PASSWORD добавлен в /opt/gendesign/tradein-mvp/.env.runtime (сгенерирован на сервере, append, chmod 600);
  • TRADEIN_DATABASE_URL добавлен в /opt/gendesign/backend/.env.runtime (тот же пароль).

После merge PR #1185 ничего вручную делать не нужно: tradein-деплой создаст роль (м.101) и выставит пароль bootstrap-шагом; gendesign-деплой пересоздаст backend/worker (compose networks изменились) и подхватит URL. QA после деплоя: admin-trigger ETL → сверка счётчиков.

✅ Ручные шаги из предыдущего коммента **уже выполнены** (2026-06-12, по указанию Anton, через ssh): - `TRADEIN_READER_PASSWORD` добавлен в `/opt/gendesign/tradein-mvp/.env.runtime` (сгенерирован на сервере, append, chmod 600); - `TRADEIN_DATABASE_URL` добавлен в `/opt/gendesign/backend/.env.runtime` (тот же пароль). После merge PR #1185 ничего вручную делать не нужно: tradein-деплой создаст роль (м.101) и выставит пароль bootstrap-шагом; gendesign-деплой пересоздаст backend/worker (compose networks изменились) и подхватит URL. QA после деплоя: admin-trigger ETL → сверка счётчиков.
lekss361 added
status/qa
and removed
status/review
labels 2026-06-12 09:02:34 +00:00
Author
Owner

QA PASS — закрываю (прод-верификация qa-tester, 2026-06-12 ~12:10):

  • Прод на 9b06377 (merge PR #1185); миграция 101 в _schema_migrations, роль gendesign_reader создана, гранты ровно на 5 таблиц.
  • gendesign-backend-1 и gendesign-worker-1 — в сетях gendesign_default + gendesign_shared; TRADEIN_DATABASE_URL в env (recreate не потребовался).
  • End-to-end: psycopg-коннект из gendesign-backend → tradein SELECT COUNT(*) FROM houses = 13382.
  • ETL прогон 1: {source_rows: 10782, upserted: 10782, skipped: 0}; прогон 2 идентичен — идемпотентность подтверждена.
  • Счётчики сходятся: tradein houses с координатами = 9193 = gendesign newbuilding_listings 9193 (source_rows 10782 — дубли LEFT JOIN house_sources, схлопываются ON CONFLICT).
  • Логи без Traceback, оба /health 200.

Nightly beat (00:30 UTC) уже в коде PR #1185. Канал market-слоя работает. Разблокирован #999 (4-й слой карты — после API-экспозиции newbuilding в analyze) и #1191 (GRANT listings, PR создан).

✅ **QA PASS — закрываю** (прод-верификация qa-tester, 2026-06-12 ~12:10): - Прод на 9b06377 (merge PR #1185); миграция 101 в `_schema_migrations`, роль `gendesign_reader` создана, гранты ровно на 5 таблиц. - `gendesign-backend-1` и `gendesign-worker-1` — в сетях `gendesign_default` + `gendesign_shared`; `TRADEIN_DATABASE_URL` в env (recreate не потребовался). - **End-to-end:** psycopg-коннект из gendesign-backend → tradein `SELECT COUNT(*) FROM houses` = **13382**. - **ETL прогон 1:** `{source_rows: 10782, upserted: 10782, skipped: 0}`; **прогон 2 идентичен** — идемпотентность подтверждена. - **Счётчики сходятся:** tradein houses с координатами = **9193** = gendesign `newbuilding_listings` 9193 (source_rows 10782 — дубли LEFT JOIN house_sources, схлопываются ON CONFLICT). - Логи без Traceback, оба /health 200. Nightly beat (00:30 UTC) уже в коде PR #1185. Канал market-слоя работает. Разблокирован #999 (4-й слой карты — после API-экспозиции newbuilding в analyze) и #1191 (GRANT listings, PR создан).
lekss361 added
status/done
and removed
status/qa
labels 2026-06-12 09:06:04 +00:00
Sign in to join this conversation.
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#976
No description provided.