Commit graph

383 commits

Author SHA1 Message Date
FluxCD
818b270a0a chore: update prop image to git.mcintire.me/graham/prop:main-1776710473-1a1ba46 [skip ci] 2026-04-20 18:44:39 +00:00
1a1ba460e4
fix(otel): drop opentelemetry_ecto + disable OTLP on Elixir pods
Ecto auto-instrumentation created one span per query, so Oban jobs
running thousands of DB ops produced traces with 8k+ child spans.
Tempo's compactor couldn't ingest them — it stalled long enough to
miss heartbeats, got auto-forgotten from the ring, and crash-looped.
The Elixir grpcbox exporter then blew up trying to reconnect.

Stops the cascade: OTLP endpoint removed from prop and prop-backfill
Deployments, opentelemetry_ecto removed from deps, Phoenix/Oban/Bandit
auto-instrumentation retained. Rust workers still export to Tempo.
2026-04-20 13:41:01 -05:00
FluxCD
5f4c02fb46 chore: update prop image to git.mcintire.me/graham/prop-grid-rs:main-1776709289-ed2563f [skip ci] 2026-04-20 18:39:39 +00:00
FluxCD
67c6d74fcf chore: update prop image to git.mcintire.me/graham/prop:main-1776709289-ed2563f [skip ci] 2026-04-20 18:34:26 +00:00
efa3cc804d
perf(rust): disk-backed HRRR idx cache + scale workers off talos5
Shared NFS idx cache at /data/hrrr_idx lets all prop-grid-rs and
hrrr-point-rs replicas deduplicate redundant NOAA S3 idx fetches.
Opt-in via HRRR_IDX_CACHE_DIR; falls back to in-memory-only when unset.

Also scales prop-grid-rs to 3 replicas (no talos5 pinning),
hrrr-point-rs to 2 replicas, and drops hot pod replicas 4→3 so the
physical-host anti-affinity still fits with talos5 cordoned off for
Postgres.
2026-04-20 13:18:38 -05:00
FluxCD
e2a2ad42fd chore: update prop image to git.mcintire.me/graham/prop:main-1776705188-b6b5945 [skip ci] 2026-04-20 17:17:04 +00:00
FluxCD
456f674d92 chore: update prop image to git.mcintire.me/graham/prop:main-1776704597-55251df [skip ci] 2026-04-20 17:11:01 +00:00
FluxCD
648bccb108 chore: update prop image to git.mcintire.me/graham/prop-grid-rs:main-1776704403-65a1b1e [skip ci] 2026-04-20 17:03:55 +00:00
55251df8a0
feat(elixir): OpenTelemetry trace export to cluster OTel Collector
Adds the OTel deps (opentelemetry + exporter + phoenix / ecto / oban /
bandit auto-instrumentation helpers) and attaches them in
Application.start/2 so the existing Phoenix, Ecto, Oban, and Bandit
telemetry events flow as OTLP spans without any call-site changes.

Exporter config is gated on OTEL_EXPORTER_OTLP_ENDPOINT in
config/runtime.exs — set in the k8s manifests to the cluster
collector (otel-collector.observability.svc.cluster.local:4317).
When unset we switch traces_exporter to :none so nothing is shipped;
dev/test stays quiet.

Resource attributes tag spans with service.name=microwaveprop and
service.namespace=prop, matching the Rust workers' attribute shape
so Tempo can group the full hourly chain across both languages.

Both the main prop deployment and the backfill deployment get the
env; backfill is still a full BEAM node running enrichment workers,
so its Oban/Ecto spans are worth seeing too.
2026-04-20 12:02:15 -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
FluxCD
a52d2e2b93 chore: update prop image to git.mcintire.me/graham/prop:main-1776694898-8089a16 [skip ci] 2026-04-20 14:26:18 +00:00
FluxCD
cfcc0a412f chore: update prop image to git.mcintire.me/graham/prop-grid-rs:main-1776694896-8089a16 [skip ci] 2026-04-20 14:25:16 +00: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
FluxCD
48da629147 chore: update prop image to git.mcintire.me/graham/prop:main-1776693990-68910f4 [skip ci] 2026-04-20 14:09:13 +00:00
68910f4d72
k8s(grid-rs): drop parallelism to 1 after 3 Gi still OOMs at 2
Two concurrent forecast tasks plus the NFS score-file write cache (23
files x ~2 MB) plus the analysis step's wgrib2 peak repeatedly crossed
the 3 Gi cgroup limit. Single-lane parallelism per pod keeps steady RSS
under 2 Gi; with 2 replicas that still gives 2 concurrent tasks
cluster-wide.

PROP_GRID_RS_PG_CONNS follows the parallelism+2 formula, dropping from
4 to 3.
2026-04-20 09:06:17 -05:00
FluxCD
1135e4b156 chore: update prop image to git.mcintire.me/graham/prop-grid-rs:main-1776693159-141956b [skip ci] 2026-04-20 14:04:09 +00:00
FluxCD
f05f80ef91 chore: update prop image to git.mcintire.me/graham/prop:main-1776693158-141956b [skip ci] 2026-04-20 13:54:56 +00:00
FluxCD
f671114105 chore: update prop image to git.mcintire.me/graham/prop:main-1776692049-1cbc3e5 [skip ci] 2026-04-20 13:36:51 +00:00
FluxCD
ab653f37ef chore: update prop image to git.mcintire.me/graham/prop:main-1776691261-a3cfff3 [skip ci] 2026-04-20 13:24:35 +00:00
a3cfff3048
fix(grid-rs): bump memory 2→3 Gi, parallelism 3→2, re-trigger Rust CI
Two bugs surfacing 13 h after Stream A cutover:

1. prop-grid-rs pods OOMKilled 16–18× overnight on the analysis step.
   f00's native-level GRIB2 (~530 MB + wgrib2 working set) runs on top
   of 2 concurrent forecast tasks, briefly reaching ~2 Gi. 2 Gi limit
   was too tight — 3 Gi plus parallelism 3→2 gives the analysis step
   room without eliminating forecast-lane headroom.

2. hrrr-point-rs pod is CrashLoopBackOff because its container is
   running image main-1776640915-65f7963 (pre-Stream-C) which doesn't
   contain the hrrr_point_worker binary. The grid-rs CI run for
   commit 4fefb81 failed and no newer image got published. Touch the
   Dockerfile to re-trigger the workflow so flux picks up a new tag
   with both binaries. No functional Dockerfile change, just a
   docstring update so the path-filter kicks.

Forecast + analysis both claim per FOR UPDATE SKIP LOCKED, so
reducing per-pod parallelism doesn't break the chain — the two
replicas cover 4 slots cluster-wide, which still drains f01..f18 in
~5 min.
2026-04-20 08:20:48 -05:00
FluxCD
2421bf562a chore: update prop image to git.mcintire.me/graham/prop:main-1776641293-4fefb81 [skip ci] 2026-04-19 23:31:18 +00:00
FluxCD
f481f11df1 chore: update prop image to git.mcintire.me/graham/prop-grid-rs:main-1776640915-65f7963 [skip ci] 2026-04-19 23:28:35 +00: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
FluxCD
586222be63 chore: update prop image to git.mcintire.me/graham/prop:main-1776640955-7416583 [skip ci] 2026-04-19 23:27:05 +00:00
FluxCD
ab784daff7 chore: update prop image to git.mcintire.me/graham/prop-grid-rs:main-1776640915-65f7963 [skip ci] 2026-04-19 23:23:00 +00:00
FluxCD
03b8c4d5d4 chore: update prop image to git.mcintire.me/graham/prop:main-1776640446-cd7f2fc [skip ci] 2026-04-19 23:16:58 +00:00
FluxCD
47251394c3 chore: update prop image to git.mcintire.me/graham/prop:main-1776640237-65f7963 [skip ci] 2026-04-19 23:14:56 +00:00
FluxCD
6613804bbf chore: update prop image to git.mcintire.me/graham/prop:main-1776639595-1307b1d [skip ci] 2026-04-19 23:02:50 +00:00
FluxCD
8434b09af1 chore: update prop image to git.mcintire.me/graham/prop-grid-rs:main-1776639595-1307b1d [skip ci] 2026-04-19 23:00:49 +00:00
FluxCD
9324c92e1a chore: update prop image to git.mcintire.me/graham/prop:main-1776639367-22f5828 [skip ci] 2026-04-19 22:59:47 +00:00
FluxCD
d145dad3c7 chore: update prop image to git.mcintire.me/graham/prop-grid-rs:main-1776639366-22f5828 [skip ci] 2026-04-19 22:57:49 +00:00
FluxCD
236108d433 chore: update prop image to git.mcintire.me/graham/prop-grid-rs:main-1776638909-afbdbbc [skip ci] 2026-04-19 22:49:39 +00:00
FluxCD
94d856153e chore: update prop image to git.mcintire.me/graham/prop-grid-rs:main-1776638384-f26e0dc [skip ci] 2026-04-19 22:41:25 +00:00
f26e0dc226
perf(prop-grid-rs): parallel band scoring + metrics + HA
Three independent improvements in one commit:

1. Parallel band scoring (pipeline.rs):
   - rayon par_iter across 23 bands replaces the serial for loop.
   - All band files now write via try_join_all over spawn_blocking
     instead of serializing one-at-a-time on NFS. Chain per-step
     drops from ~60s to ~15-25s on a 4-core box.

2. Prometheus metrics (new metrics.rs):
   - axum server on METRICS_ADDR (default :9100) exposes /metrics and
     /health. Scraped by existing Prometheus via annotation.
   - Histograms: chain step duration (by outcome), decode duration.
   - Gauge: tasks in flight (RAII guarded so panics don't leak).
   - Counter: chain steps by outcome.
   - worker.rs records step duration and wraps each run in an
     InFlightGuard.

3. HA + ordering fixes:
   - deployment-grid-rs.yaml: replicas 1→2, required podAntiAffinity
     spreads them across hosts, nodeAffinity prefers talos5 for one.
     PROP_GRID_RS_PARALLELISM 4→3 per pod (6 concurrent across
     replicas; smaller secondary-node footprint).
   - Readiness probe on /health so rollouts wait for the runtime to
     be alive.
   - claim_next ordering: run_time ASC → DESC so the newest hourly
     run drains before any stragglers from a broken prior run. Within
     a run, forecast_hour ASC keeps nearest-first.
   - grid_task_enqueuer sets kind="forecast" explicitly and updates
     conflict_target to the new 3-column unique index introduced by
     migration 20260419222624.
2026-04-19 17:39:30 -05:00
FluxCD
dcb68089db chore: update prop image to git.mcintire.me/graham/prop:main-1776637893-022e692 [skip ci] 2026-04-19 22:34:26 +00:00
FluxCD
65d7122c36 chore: update prop image to git.mcintire.me/graham/prop-grid-rs:main-1776637891-022e692 [skip ci] 2026-04-19 22:32:38 +00:00
022e6928bc
perf(prop-grid-rs): claim N forecast hours concurrently
Replaces the serial claim→process loop with a JoinSet of N worker tasks,
each running the same claim→fetch→decode→score→complete flow against
grid_tasks. FOR UPDATE SKIP LOCKED already prevents contention between
workers so no additional coordination is needed.

talos5 is 4c/4t (i5-7260U); PROP_GRID_RS_PARALLELISM=4 turns the full
f01..f18 chain from ~18 min serial into roughly ~5 min. Per-task peak
memory after the streamed-bands refactor is ~250 Mi, so 4 concurrent is
~1 Gi — well under the 2 Gi container limit.

Postgres pool sized to parallelism + 2 so NOTIFY and an opportunistic
claim retry share the pool without blocking a worker transaction.
2026-04-19 17:31:16 -05:00
FluxCD
ed6b16dc60 chore: update prop image to git.mcintire.me/graham/prop:main-1776637573-4870862 [skip ci] 2026-04-19 22:29:59 +00:00
FluxCD
676999606b chore: update prop image to git.mcintire.me/graham/prop:main-1776637110-0528727 [skip ci] 2026-04-19 22:22:50 +00:00
FluxCD
3572a15c4c chore: update prop image to git.mcintire.me/graham/prop:main-1776636597-234a27b [skip ci] 2026-04-19 22:12:47 +00:00
FluxCD
52c062c90a chore: update prop image to git.mcintire.me/graham/prop:main-1776635907-6c73012 [skip ci] 2026-04-19 22:01:40 +00:00
6c73012d09
fix(k8s): tier label isolates hot pods from backfill endpoints
Both the hot (`prop`) Deployment and the `prop-backfill` Deployment
labelled their pods with `app: prop` so both joined the same libcluster
Erlang cluster. The `prop` Service selector was `app: prop` too — which
meant the backfill pod (PHX_SERVER=false, no listener on :5000) was in
the Service endpoint pool.

With sessionAffinity: ClientIP, Cloudflared's source IP got hashed to
the backfill endpoint after a backfill pod roll and every request
from that client returned 502 (connection refused on :5000).

Fix: add `tier: hot` to hot-pod template labels; narrow the Service
selector to `{app: prop, tier: hot}`. Backfill keeps `tier: backfill`
and is cleanly excluded. libcluster selector (still `app: prop`) is
unchanged so backfill stays in the Erlang cluster for Oban queue
leader election and Oban.Pro's Smart engine.
2026-04-19 16:58:13 -05:00
FluxCD
8ab2d836fb chore: update prop image to git.mcintire.me/graham/prop:main-1776635487-65693ed [skip ci] 2026-04-19 21:54:34 +00:00
65693ed415
feat(prop): Phase 2 cutover — Rust owns f01–f18
Rust `prop-grid-rs` has been validated end-to-end on talos5 on the
streamed-bands image (main-1776635096-6d91461): real chain steps
complete in ~7 s each and the peak-heap refactor landed.

Changes:
- `PropagationGridWorker.seed_chain` now enqueues a single f00 Oban
  job instead of f00–f18. The f00 analysis-hour keeps its enrichment
  (native-level duct, NEXRAD composite, commercial-link boost,
  ProfilesFile write) and the GridCache broadcast.
- `GridTaskEnqueuer.seed/1` is called again so grid_tasks gets the
  18 forecast-hour rows the Rust worker claims.
- Test asserts only one Oban job is enqueued and the matching 18
  grid_tasks rows land in the DB.
- `deployment-grid-rs.yaml` flips `PROP_SCORES_DIR` from
  `/data/scores_shadow` to `/data/scores` so Rust writes to the live
  score tree. No file collision — Elixir only writes the f00 files.

The hot pod memory shrink (6 Gi → 1 Gi) is deferred until one full
hourly cycle runs clean under the new split.
2026-04-19 16:51:14 -05:00
FluxCD
54bc46ab60 chore: update prop image to git.mcintire.me/graham/prop:main-1776635090-6d91461 [skip ci] 2026-04-19 21:47:29 +00:00
FluxCD
76a407c8b1 chore: update prop image to git.mcintire.me/graham/prop-grid-rs:main-1776635096-6d91461 [skip ci] 2026-04-19 21:46:29 +00:00
FluxCD
ae4b0742fb chore: update prop image to git.mcintire.me/graham/prop:main-1776634649-ca33b83 [skip ci] 2026-04-19 21:41:27 +00:00
ca33b8331b
fix(grid-rs): bump memory limit to 2 Gi
1 Gi OOM'd during a real chain step. Each forecast hour loads both a
surface and pressure-level HRRR GRIB2 (~40 MB combined), decodes via
wgrib2 subprocess (which allocates its own buffers), scores 92k grid
points × 23 bands (~2.1M intermediates), and writes 23 score files.
2 Gi absorbs the spike; still ~3× smaller than the Elixir-per-pod
budget we're replacing.
2026-04-19 16:37:05 -05:00
25c3d4f688
fix(grid-rs): bump memory limit to 1 Gi
The real worker binary OOM-killed at 512 Mi on startup. The plan's
512 Mi assumption was speculative — actual Rust RSS at boot includes
sqlx pool buffers, reqwest connection pool, tokio runtime, and
tracing JSON subscriber. 1 Gi leaves room for all of that plus the
steady-state ~500 Mi during a chain step.
2026-04-19 16:35:18 -05:00
FluxCD
90281b8562 chore: update prop image to git.mcintire.me/graham/prop-grid-rs:main-1776634333-2d67cf7 [skip ci] 2026-04-19 21:32:58 +00:00