ОТКРЫТИЕ: 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 (Челябинск/Тюмень/Пермь)
85 lines
4.8 KiB
SQL
85 lines
4.8 KiB
SQL
-- 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 и продолжает.';
|