gendesign/data/sql/77_nspd_geo_jobs.sql
lekss361 54dbc77348 feat(geo): NSPD bulk-fetcher без Playwright + resume-friendly UI
ОТКРЫТИЕ: stdlib urllib проходит WAF nspd.gov.ru (TLS-fingerprint stdlib
отличается от mainstream HTTP-clients). Это позволяет полностью убрать
Playwright + Chromium для NSPD-задач.

* nspd_lite.py: urllib + ssl._create_unverified_context(). 4 публичных
  fetcher'а (geoportal/quarter/parcel/building) + fetch_via_rosreestr2coord
  fallback на community-lib

* schema 77: nspd_geo_jobs (журнал) + nspd_geo_targets (cad-номера со
  статусом pending/done/failed). Resume-state в БД.

* tasks/nspd_geo.py: Celery task process_nspd_geo_job(job_id). Heartbeat
  каждые 5 items, WAF backoff 30s × 2^N (max 8 → paused). UPSERT в
  cad_quarters_geom / cad_buildings → идемпотентно.

* worker_ready hook: stale jobs (>10min без heartbeat) автоматически
  re-enqueue после redeploy. Никаких потерь прогресса.

* 4 admin endpoint + UI /admin/scrape/geo с формой запуска (manual_list /
  rosreestr_pending для авто-наполнения из ДДУ-кварталов), progress-bars
  и cancel/resume кнопками per job.

* settings: use_nspd_lite=True, nspd_lite_rate_ms=600.
* dep: rosreestr2coord>=4.0.0 (community lib для fallback).

TODO следующих шагов:
- smoke-test на проде через SSH (validation RU-IP не банит urllib)
- feature toggle USE_NSPD_LITE в scrape_nspd.py для переключения с старого
  Playwright-пути
- docker lean image (-300 MB после убирания Chromium)
- расширение SCRAPE_NSPD_DEFAULT_REGIONS на 66,74,72,59 (Челябинск/Тюмень/Пермь)
2026-05-11 08:53:28 +03:00

85 lines
4.8 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- Resume-friendly NSPD geo backfill jobs.
--
-- Зачем:
-- Bulk-загрузка кадастровых данных по 100k+ cad-номеров занимает часы.
-- При redeploy worker'а Celery теряет in-memory state. Решение —
-- хранить очередь и текущий прогресс в БД, после редеплоя
-- worker_ready hook подхватывает незавершённые jobs.
--
-- Архитектура:
-- nspd_geo_jobs — общий журнал заданий (один job = одна bulk-операция)
-- nspd_geo_targets — список cad-номеров для обработки (1 строка = 1 cad)
-- со статусом (pending/done/failed) — это и есть resume-state.
--
-- При redeploy:
-- worker_ready signal → SELECT FROM nspd_geo_jobs WHERE status='running'
-- → re-enqueue Celery task с тем же job_id
-- → task SELECT FROM nspd_geo_targets WHERE job_id=:id AND status='pending'
-- → продолжает с той точки где остановился (idempotent UPSERT в cad_quarters_geom)
--
-- Применять через mcp postgres execute_sql или psql direct.
CREATE TABLE IF NOT EXISTS nspd_geo_jobs (
job_id BIGSERIAL PRIMARY KEY,
name TEXT, -- человекочитаемое имя ('УрФО expansion 2026-Q2')
job_kind TEXT NOT NULL, -- 'quarters' | 'parcels' | 'buildings' | 'mixed'
-- Источник целей
source_kind TEXT NOT NULL, -- 'rosreestr_pending' | 'manual_list' | 'region_bbox'
source_params jsonb, -- что-то вроде {"region_codes":[66,74,72,59], "thematic_id":2}
-- Параметры fetch
rate_ms INTEGER NOT NULL DEFAULT 600,
use_rosreestr2coord BOOLEAN NOT NULL DEFAULT FALSE, -- если TRUE — community-lib, иначе наш nspd_lite
-- Состояние
status TEXT NOT NULL DEFAULT 'queued', -- queued | running | done | failed | paused
started_at timestamptz,
finished_at timestamptz,
heartbeat_at timestamptz,
-- Счётчики
targets_total INTEGER NOT NULL DEFAULT 0,
targets_done INTEGER NOT NULL DEFAULT 0,
targets_failed INTEGER NOT NULL DEFAULT 0,
targets_skipped INTEGER NOT NULL DEFAULT 0, -- уже было в БД (idempotent skip)
requests_count INTEGER NOT NULL DEFAULT 0,
waf_blocked_count INTEGER NOT NULL DEFAULT 0,
-- Audit
error TEXT,
triggered_by TEXT NOT NULL DEFAULT 'manual',-- manual | beat | resume
created_at timestamptz NOT NULL DEFAULT NOW(),
updated_at timestamptz NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS nspd_geo_jobs_status_idx
ON nspd_geo_jobs(status, created_at DESC);
CREATE INDEX IF NOT EXISTS nspd_geo_jobs_running_idx
ON nspd_geo_jobs(heartbeat_at DESC) WHERE status = 'running';
COMMENT ON TABLE nspd_geo_jobs IS
'Журнал bulk geo-jobs для NSPD-fetcher. Resume-friendly: при redeploy '
'worker_ready hook подхватывает status=running с устаревшим heartbeat.';
-- ── Targets (cad-номера к обработке) ─────────────────────────────────────────
CREATE TABLE IF NOT EXISTS nspd_geo_targets (
target_id BIGSERIAL PRIMARY KEY,
job_id BIGINT NOT NULL REFERENCES nspd_geo_jobs(job_id) ON DELETE CASCADE,
cad_num TEXT NOT NULL, -- '66:41:0204016' для квартала, '...:10' для участка
thematic_id SMALLINT NOT NULL, -- 1=parcel | 2=quarter | 5=building
status TEXT NOT NULL DEFAULT 'pending', -- pending | done | failed | skipped
attempts SMALLINT NOT NULL DEFAULT 0,
-- Результат
result_features INTEGER, -- сколько features вернулось
saved_to_table TEXT, -- 'cad_quarters_geom' | 'cad_buildings' | ...
error_msg TEXT,
fetched_at timestamptz,
-- Avoid duplicate work внутри одного job
UNIQUE (job_id, cad_num, thematic_id)
);
CREATE INDEX IF NOT EXISTS nspd_geo_targets_pending_idx
ON nspd_geo_targets(job_id, status) WHERE status = 'pending';
CREATE INDEX IF NOT EXISTS nspd_geo_targets_failed_idx
ON nspd_geo_targets(job_id) WHERE status = 'failed';
COMMENT ON TABLE nspd_geo_targets IS
'Список cad-номеров для обработки в рамках job. Resume-state: '
'task просто SELECT WHERE status=pending и продолжает.';