Commit graph

7 commits

Author SHA1 Message Date
7062344434
fix(rust): snap HRRR point lookups to 0.125° grid
hrrr-point-rs was marking fetch tasks 'done' with 0 profiles inserted
for ~13,900 historical contacts. The wgrib2 subprocess resamples HRRR
to the 0.125° CONUS prop grid, but the lookup keyed on the requested
point (rounded to 0.01° by Weather.round_to_hrrr_grid/2) — those keys
never match a 0.125° cell exactly, so every point fell through the
`cell.is_empty()` skip. Snap to the nearest 0.125° cell for lookup,
store at the original (lat, lon) so Weather.has_hrrr_profile?/3's
0.07° search window finds it on the next backfill tick.

TDD: added snap_key unit tests.
2026-04-22 16:50:58 -05:00
e9a38623d8
perf+hygiene: batch 1 of system-review fixes
Batched from a system-wide review pass.

**Rate-limit + transient-error log hygiene**
- WeatherFetchWorker: classify IEM HTTP 429 as {:snooze, 300} instead
  of {:error}. Stops Oban.PerformError stack-trace spam on every
  routine rate-limit during backfill, keeps the retry semantics.
- IonosphereFetchWorker: compress GIRO TLS :unknown_ca error from a
  ~400-char inspect blob to 'TLS unknown_ca (CA bundle missing)'.
- FreshnessMonitor: log only when an enqueue actually lands (the
  Oban unique conflict case was silently dropping jobs but still
  producing 'enqueuing grid worker' info lines every 5 minutes).

**Perf**
- Rust grid_level_keys(): pre-format 'TMP:{p} mb' / DPT / HGT keys
  once via OnceLock instead of format!()'ing per-cell. Removes ~3.6M
  String allocations per f01..f18 chain step across pipeline.rs,
  hrrr_points.rs. Same for the wgrib2 :(TMP|DPT|HGT): pattern in
  hrrr_points.process_batch (sfc_pattern / prs_pattern OnceLock).
- MapLive preload_forecast: Task.async_stream with max_concurrency=4
  replaces the 18-wide Enum.map serial walk. Forecast cache warms
  ~4× faster after a band change, with ordering preserved.
- grid_tasks: partial composite index on (run_time DESC,
  forecast_hour ASC) WHERE status='queued' AND kind='forecast',
  matching the Rust claim query's ORDER BY. Drops the old
  status-only partial that forced a sort per claim.

**Correctness**
- ContactImportWorker: add Oban unique:[keys: [:import_run_id,
  :offset]]. Was missing on a worker whose perform() does a
  non-idempotent atomic counter increment — a retry would double-
  count imported rows.
- CommonVolumeRadarWorker: x_min..x_max default step is -1 when
  x_min > x_max, triggering a runtime deprecation warning. Force
  Range.new(_, _, 1) explicitly.

**Cleanup**
- Drop unused tmp_dir parameter from hrrr_point_worker + its
  process_batch signature.
2026-04-21 17:06:07 -05:00
ed2563f4f8
style(rust): apply cargo fmt across prop_grid_rs 2026-04-20 13:20:22 -05:00
65a1b1edb3
feat(rust): OTLP trace export to cluster OTel Collector
New telemetry module wires tracing-subscriber to both a local JSON
fmt layer (keeps kubectl-logs output identical) and an
OpenTelemetry OTLP/gRPC exporter, activated only when
OTEL_EXPORTER_OTLP_ENDPOINT is set. The returned TelemetryGuard
holds the SdkTracerProvider until process shutdown so queued spans
flush before exit.

Both bin targets (worker, hrrr_point_worker) now call
telemetry::init(service_name) at startup; service_name becomes the
OTel service.name attribute so Tempo groups spans per binary.

Tracing instrumentation on the three main work units:
- pipeline::run_chain_step (forecast f01..f18)
- pipeline::run_analysis_step (analysis f00)
- hrrr_points::process_batch (per-QSO point drain)

k8s manifests set OTEL_EXPORTER_OTLP_ENDPOINT to the cluster
collector at otel-collector.observability.svc.cluster.local:4317.
Backend wiring lives in the vntx-infra repo.
2026-04-20 11:59:49 -05:00
a1d2e9f94a
fix(hrrr-points): bind jsonb[] correctly, warn on empty fetch, 2 Gi limit
Three fixes after hrrr-point-rs restarts on the first live drain:

1. Type mismatch (hard error): the `profile` column on `hrrr_profiles`
   is `jsonb[]` (one element per pressure level), but the Rust worker
   was binding a single jsonb array-of-objects cast as `::jsonb`. Every
   insert failed with `column "profile" is of type jsonb[] but
   expression is of type jsonb`. Switched to
   `Vec<sqlx::types::Json<Value>>`, dropped the explicit `::jsonb`
   cast, and enabled sqlx's `json` feature so the encoder maps the
   array into `_jsonb` correctly.

2. Silent zero-inserts: four consecutive 2019-09-22 batches completed
   with `profiles_inserted: 0` and no diagnostic. Most likely the
   upstream archive doesn't keep cycles that old, but without a log
   it looks identical to a snap-mismatch bug. Added a WARN when both
   surface and pressure grids come back empty so fetch-miss vs.
   projection-miss is distinguishable.

3. OOMKilled: the pod took four OOM restarts in ten minutes at 1 Gi.
   A single CONUS decode holds the ~40 MB blob, the ~200 MB wgrib2
   working set, AND the full 92k-cell merged map until all requested
   points drain. 1 Gi has no headroom. Bumped to 2 Gi, matching the
   rest of the per-container budgets in this namespace.
2026-04-20 09:18:52 -05:00
ea73f3164d
fix(grid-rs): drop target/ cache mount, simplify Dockerfile
The docker buildx build step has failed twice since Stream C added the
hrrr_point_worker binary. Dropping the target/ BuildKit cache mount
eliminates the most likely cause (disk pressure inside a persistent
cache shared between the two bin builds). Full rebuild from scratch
adds ~3 min to the build but the failure mode is worth more than the
cache.

Also dropped the empty unit test scaffolding in hrrr_points.rs; the
module is thin glue over fetcher+decoder which already carry coverage.
Integration tests for process_batch/upsert_profile are gated on a live
DB + HRRR mirror and live in tests/ (when added).
2026-04-20 08:30:49 -05:00
4fefb81c24
feat(hrrr-point-rs): Rust binary for per-QSO HRRR enrichment
Phase 3 Stream C Rust side. Completes the HrrrFetchWorker port.

Pipeline:
- db::claim_next_hrrr_task — FOR UPDATE SKIP LOCKED on hrrr_fetch_tasks,
  newest valid_time first. Accepts the points JSONB directly.
- hrrr_points::process_batch — fetch surface + pressure GRIB2 once
  per task (tokio::try_join), decode via the existing wgrib2 plumbing,
  then for each requested point pull the cell and UPSERT INTO
  hrrr_profiles (conflict on lat/lon/valid_time).
- db::complete_hrrr_task / fail_hrrr_task — status transitions; Elixir
  backfill re-enqueues failed rows on next /30-min scan.

Shipping pieces:
- new bin src/bin/hrrr_point_worker.rs
- new module src/hrrr_points.rs (process_batch, upsert_profile)
- new Cargo [[bin]] entry; Dockerfile builds both binaries in one stage
  and ships them in the runtime image so a single CI pipeline covers
  the whole cluster
- k8s/deployment-hrrr-point-rs.yaml (1 replica, 1 Gi limit, anti-affinity
  against prop-grid-rs so chain + point work don't fight for wgrib2
  slots). Uses the same image; command: override picks the right binary.
- kustomization.yaml: include the new deployment so flux applies it
- deployment-grid-rs.yaml: bump readiness initialDelaySeconds 3→15 +
  failureThreshold 3→6 so a slow DB connect during startup can't race
  the first probe

119 Rust tests green.
2026-04-19 18:28:00 -05:00