-- 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 (всегда через rosreestr2coord lib v5+; -- legacy колонка `use_rosreestr2coord` удалена миграцией 78_). rate_ms INTEGER NOT NULL DEFAULT 600, -- Состояние 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 и продолжает.'; -- ── nspd_geo_log: логи (видны через v_scrape_log_unified) ─────────────────── CREATE TABLE IF NOT EXISTS nspd_geo_log ( log_id BIGSERIAL PRIMARY KEY, job_id BIGINT REFERENCES nspd_geo_jobs(job_id) ON DELETE CASCADE, ts timestamptz NOT NULL DEFAULT NOW(), level TEXT NOT NULL DEFAULT 'info', stage TEXT, cad_num TEXT, message TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS nspd_geo_log_job_idx ON nspd_geo_log(job_id, log_id DESC); CREATE INDEX IF NOT EXISTS nspd_geo_log_ts_idx ON nspd_geo_log(ts DESC); -- ── Расширение unified-views на nspd_geo (UNION ALL) ──────────────────────── CREATE OR REPLACE VIEW v_scrape_runs_unified AS SELECT 'kn' AS scraper_type, run_id, started_at, finished_at, heartbeat_at, status, error, NULL::text AS triggered_by, region_codes::text AS scope, requests_count, objects_count AS items_ok, NULL::integer AS items_failed, flats_count AS sub_items_ok, progress_obj_index AS progress_idx, total_obj_count AS progress_total, jsonb_build_object('developers', developer_ids, 'snapshot_date', snapshot_date, 'resumed_from', resumed_from_run_id, 'params', params) AS extra FROM kn_scrape_runs UNION ALL SELECT 'nspd', run_id, started_at, finished_at, heartbeat_at, status, error, triggered_by, region_code::text, requests_count, quarters_ok, quarters_failed, buildings_ok, NULL, pending_count, jsonb_build_object('waf_429_count', waf_429_count) FROM nspd_scrape_runs UNION ALL SELECT 'objective', run_id, started_at, finished_at, heartbeat_at, status, error, triggered_by, group_name, requests_count, reports_ok, reports_failed, rows_lots, NULL, NULL, jsonb_build_object('rows_corpus_room', rows_corpus_room, 'rows_history', rows_history) FROM objective_scrape_runs UNION ALL SELECT 'nspd_geo', job_id, started_at, finished_at, heartbeat_at, status, error, triggered_by, COALESCE(name, job_kind), requests_count, targets_done, targets_failed, targets_skipped, targets_done, targets_total, jsonb_build_object('job_kind', job_kind, 'source_kind', source_kind, 'rate_ms', rate_ms, 'waf_blocked_count', waf_blocked_count, 'source_params', source_params) FROM nspd_geo_jobs; CREATE OR REPLACE VIEW v_scrape_log_unified AS SELECT 'kn' AS scraper_type, log_id, run_id, ts, level, stage, obj_id::text AS entity_id, message FROM kn_scrape_log UNION ALL SELECT 'nspd', log_id, run_id, ts, level, stage, cad_number, message FROM nspd_scrape_log UNION ALL SELECT 'nspd_geo', log_id, job_id AS run_id, ts, level, stage, cad_num, message FROM nspd_geo_log;