perf(cadastre): freshness skip + grid-walk heartbeat + reduce WMS log noise
Three operational improvements after ekb_full v3 completed at 99.1%: Fix H — per-quarter freshness skip (scrape_cadastre.py): - enqueue_cadastre_harvest queries cad_quarter_stats.fetched_at per quarter - Skip quarters where fetched_at >= NOW() - skip_fresh_hours (default 1h) - extra_config.skip_fresh_hours=0 disables skip (full re-fetch) - Saves ~30k NSPD calls on repeated runs of same scope Fix I — heartbeat in grid-walk loop (bulk_harvest.py:_grid_walk_category): - 225 cells x 70-300ms = 15-75s heartbeat silence - Pass update_progress callback to _grid_walk_category - Call every heartbeat_every (50) cells with phase progress info - Prevents cleanup_zombies false-positive cancellation Fix J — NSPD WMS HTTP 500 noise to debug (bulk_harvest.py:_grid_walk_category): - NSPD WMS endpoint frequently returns HTTP 500 (server-side issue) - Was: logger.warning each, 200+ warnings per quarter - Now: logger.debug (visible via --log-level=DEBUG) 40 tests passing.
This commit is contained in:
parent
d104222643
commit
ae04dcb4b9
2 changed files with 71 additions and 5 deletions
|
|
@ -218,6 +218,7 @@ async def harvest_quarter(
|
||||||
client=client,
|
client=client,
|
||||||
quarter=quarter,
|
quarter=quarter,
|
||||||
layer_id=cat_id,
|
layer_id=cat_id,
|
||||||
|
update_progress=update_progress,
|
||||||
)
|
)
|
||||||
result.grid_walk_requests += n_requests
|
result.grid_walk_requests += n_requests
|
||||||
|
|
||||||
|
|
@ -276,12 +277,19 @@ async def _grid_walk_category(
|
||||||
layer_id: int,
|
layer_id: int,
|
||||||
grid_size: int = 15,
|
grid_size: int = 15,
|
||||||
tile_size: int = 512,
|
tile_size: int = 512,
|
||||||
|
update_progress: Callable[[dict[str, Any]], None] | None = None,
|
||||||
|
heartbeat_every: int = 50,
|
||||||
) -> tuple[int, int]:
|
) -> tuple[int, int]:
|
||||||
"""Grid-walk layer_id в bbox квартала.
|
"""Grid-walk layer_id в bbox квартала.
|
||||||
|
|
||||||
Генерирует grid_size×grid_size ячеек, для каждой делает wms_feature_info.
|
Генерирует grid_size×grid_size ячеек, для каждой делает wms_feature_info.
|
||||||
Дедуплицирует по cad_num, upsert'ит новые объекты.
|
Дедуплицирует по cad_num, upsert'ит новые объекты.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
update_progress: Fix I — callback для heartbeat внутри grid-walk loop.
|
||||||
|
225 cells × ~70ms = ~15-75s без heartbeat, zombie-cleanup ложно
|
||||||
|
помечал worker как мёртвого. Вызываем каждые heartbeat_every cells.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(upserted_count, requests_count)
|
(upserted_count, requests_count)
|
||||||
"""
|
"""
|
||||||
|
|
@ -300,7 +308,19 @@ async def _grid_walk_category(
|
||||||
upserted = 0
|
upserted = 0
|
||||||
requests = 0
|
requests = 0
|
||||||
|
|
||||||
for cell_bbox, click_xy in grid_points:
|
for idx, (cell_bbox, click_xy) in enumerate(grid_points):
|
||||||
|
# Fix I: heartbeat каждые N cells — обновляет cadastre_jobs.heartbeat_at
|
||||||
|
# чтобы cleanup_zombies (5min stale check) не cancel'нул живой worker.
|
||||||
|
if update_progress is not None and idx > 0 and idx % heartbeat_every == 0:
|
||||||
|
update_progress(
|
||||||
|
{
|
||||||
|
"phase": f"grid_walk_{layer_id}_cell_{idx}",
|
||||||
|
"quarter": quarter,
|
||||||
|
"grid_cells_done": idx,
|
||||||
|
"grid_cells_total": len(grid_points),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
features = await client.wms_feature_info(
|
features = await client.wms_feature_info(
|
||||||
layer_id=layer_id,
|
layer_id=layer_id,
|
||||||
|
|
@ -311,7 +331,10 @@ async def _grid_walk_category(
|
||||||
)
|
)
|
||||||
requests += 1
|
requests += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(
|
# Fix J: NSPD WMS возвращает HTTP 500 на десятки cells per quarter —
|
||||||
|
# это server-side noise, не наш bug. logger.debug чтобы не засорять
|
||||||
|
# prod логи (раньше было warning → 200+ warnings per quarter).
|
||||||
|
logger.debug(
|
||||||
"_grid_walk_category: wms_feature_info error layer=%d quarter=%s: %s",
|
"_grid_walk_category: wms_feature_info error layer=%d quarter=%s: %s",
|
||||||
layer_id,
|
layer_id,
|
||||||
quarter,
|
quarter,
|
||||||
|
|
|
||||||
|
|
@ -357,22 +357,65 @@ def enqueue_cadastre_harvest(self: Any, job_id: int) -> dict[str, Any]:
|
||||||
logger.warning("enqueue_cadastre_harvest: job=%d имеет пустой список quarters", job_id)
|
logger.warning("enqueue_cadastre_harvest: job=%d имеет пустой список quarters", job_id)
|
||||||
return {"job_id": job_id, "enqueued": 0}
|
return {"job_id": job_id, "enqueued": 0}
|
||||||
|
|
||||||
|
# Fix H: per-quarter freshness skip — не переобрабатывать quarters с свежим
|
||||||
|
# cad_quarter_stats.fetched_at (последний успешный обход <FRESHNESS_HOURS).
|
||||||
|
# extra_config.skip_fresh_hours=N управляет порогом; 0 → отключает skip.
|
||||||
|
# Это сэкономит ~30k NSPD calls при повторных запусках того же scope.
|
||||||
|
extra_config = job["extra_config"] or {}
|
||||||
|
skip_fresh_hours = extra_config.get("skip_fresh_hours", 1)
|
||||||
|
skipped_fresh: set[str] = set()
|
||||||
|
if skip_fresh_hours > 0:
|
||||||
|
threshold = _dt.datetime.now(_dt.UTC) - _dt.timedelta(hours=skip_fresh_hours)
|
||||||
|
rows = (
|
||||||
|
db.execute(
|
||||||
|
text(
|
||||||
|
"SELECT cad_number FROM cad_quarter_stats "
|
||||||
|
"WHERE cad_number = ANY(CAST(:qs AS text[])) "
|
||||||
|
"AND fetched_at >= :t"
|
||||||
|
),
|
||||||
|
{"qs": quarters, "t": threshold},
|
||||||
|
)
|
||||||
|
.scalars()
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
skipped_fresh = set(rows)
|
||||||
|
if skipped_fresh:
|
||||||
|
# Корректируем targets_total — skipped quarters не входят в прогресс
|
||||||
|
db.execute(
|
||||||
|
text(
|
||||||
|
"UPDATE cadastre_jobs SET targets_total = :new_total, "
|
||||||
|
"targets_skipped = COALESCE(targets_skipped, 0) + :sk "
|
||||||
|
"WHERE job_id = :id"
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"new_total": len(quarters) - len(skipped_fresh),
|
||||||
|
"sk": len(skipped_fresh),
|
||||||
|
"id": job_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# Обновить статус на running
|
# Обновить статус на running
|
||||||
_update_job(db, job_id, status="running", started_at=_dt.datetime.now(_dt.UTC))
|
_update_job(db, job_id, status="running", started_at=_dt.datetime.now(_dt.UTC))
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
enqueued = 0
|
||||||
for quarter in quarters:
|
for quarter in quarters:
|
||||||
|
if quarter in skipped_fresh:
|
||||||
|
continue
|
||||||
bulk_harvest_quarter_task.apply_async(
|
bulk_harvest_quarter_task.apply_async(
|
||||||
args=[quarter, job_id],
|
args=[quarter, job_id],
|
||||||
queue=CADASTRE_QUEUE,
|
queue=CADASTRE_QUEUE,
|
||||||
)
|
)
|
||||||
|
enqueued += 1
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"enqueue_cadastre_harvest: job=%d enqueued %d quarters",
|
"enqueue_cadastre_harvest: job=%d enqueued %d (skipped_fresh=%d, threshold=%dh)",
|
||||||
job_id,
|
job_id,
|
||||||
len(quarters),
|
enqueued,
|
||||||
|
len(skipped_fresh),
|
||||||
|
skip_fresh_hours,
|
||||||
)
|
)
|
||||||
return {"job_id": job_id, "enqueued": len(quarters)}
|
return {"job_id": job_id, "enqueued": enqueued, "skipped_fresh": len(skipped_fresh)}
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue