Commit graph

554 commits

Author SHA1 Message Date
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
FluxCD
e7496331ba chore: update prop image to git.mcintire.me/graham/prop:main-1776634019-2d67cf7 [skip ci] 2026-04-19 21:31:30 +00:00
FluxCD
a40e2d1630 chore: update prop image to git.mcintire.me/graham/prop-grid-rs:main-1776633497-5692d50 [skip ci] 2026-04-19 21:25:02 +00:00
735553e443
feat(flux): add image-automation for prop-grid-rs
Mirrors the Elixir wiring:
- ImageRepository + ImagePolicy scan git.mcintire.me/graham/prop-grid-rs
  for main-<ts>-<sha> tags and pick the highest timestamp.
- ImageUpdateAutomation already covers ./k8s via the Setters strategy,
  so the new setter marker on deployment-grid-rs.yaml means flux will
  rewrite it on every CI push and commit the update back to main.
2026-04-19 16:24:33 -05:00
FluxCD
bf370ba1e4 chore: update prop image to git.mcintire.me/graham/prop:main-1776633273-03ba48f [skip ci] 2026-04-19 21:18:02 +00:00
FluxCD
04df743e82 chore: update prop image to git.mcintire.me/graham/prop:main-1776632590-6e64e08 [skip ci] 2026-04-19 21:07:57 +00:00
FluxCD
862c0a915e chore: update prop image to git.mcintire.me/graham/prop:main-1776632295-79bbdf9 [skip ci] 2026-04-19 21:01:53 +00:00
FluxCD
8d3abbc711 chore: update prop image to git.mcintire.me/graham/prop:main-1776632143-8774181 [skip ci] 2026-04-19 20:58:43 +00:00
FluxCD
9a944f40e5 chore: update prop image to git.mcintire.me/graham/prop:main-1776631843-339591c [skip ci] 2026-04-19 20:53:39 +00:00
FluxCD
f0485dd595 chore: update prop image to git.mcintire.me/graham/prop:main-1776631383-b808780 [skip ci] 2026-04-19 20:46:29 +00:00
b80878056d
feat(grid-rs): Rust worker for HRRR f01..f18 propagation chain
Extracts the memory-hostile HRRR fetch → decode → score pipeline for
forecast hours 1–18 into a separate Rust service (`prop-grid-rs`).
Elixir retains f00 with its native-duct + NEXRAD + commercial-link
enrichment; Rust handles the other 18 steps per hourly chain.

Hand-off via a new `grid_tasks` table (`FOR UPDATE SKIP LOCKED` claim
from Rust, Postgrex `NOTIFY propagation_ready` back to Elixir). Rust
writes the same on-disk score-grid format Elixir already uses, to the
same `/data/scores` NFS tree. Phase 1 ships in shadow mode with
PROP_SCORES_DIR=/data/scores_shadow.

Rust crate layout at `rust/prop_grid_rs/` (1:1 module parity with the
Elixir source it ports):
  - grid, region, band_config, scores_file, sounding_params
  - scorer: all 10 factors + composite. Matches the Elixir scorer
    byte-for-byte across 115 golden-fixture samples (5 scenarios
    × 23 bands).
  - decoder: wgrib2 subprocess + Fortran-record lola-binary parser
  - fetcher: HRRR URL derivation + idx cache (1h TTL) + 8-way
    parallel byte-range downloads with 429/5xx retry
  - pipeline: end-to-end chain step, f00 rejected at the boundary
  - db: sqlx grid_tasks claim/complete + propagation_ready NOTIFY
  - bin/worker: tokio main loop, JSON logs, SIGTERM-safe

91 Rust tests + clippy -D warnings clean. 2,158 Elixir tests green.

Elixir additions:
  - `GridTaskEnqueuer` seeds fh=1..18 rows from
    `PropagationGridWorker.seed_chain/0`
  - `NotifyListener` LISTEN → warm `ScoreCache` → PubSub fan-out
  - `ShadowComparator` diffs prod vs shadow `.ntms` bodies
  - `mix rust.golden` task writes the Rust-side golden fixture

k8s: new `deployment-grid-rs.yaml` pinned to talos5 (32 GB NUC) via
`prop-grid-rs=primary` nodeSelector + `workload=grid-rs:NoSchedule`
toleration, 512 Mi limit, sharing the existing NFS `/data` mount.

The plan document is at `plans/vivid-hatching-quail.md` (local to my
workstation); phases 2 (cutover) and 3 (talos5 concurrency tuning)
follow after 72h of shadow-mode parity.
2026-04-19 15:42:49 -05:00
FluxCD
fd95a82478 chore: update prop image to git.mcintire.me/graham/prop:main-1776628623-f1846c0 [skip ci] 2026-04-19 20:00:04 +00:00
dfc7895197
chore(infra): bump prop to 4 replicas for talos5 (NUC)
talos5 is a NUC7i5 with 32GB RAM and a Samsung 980 Pro NVMe. Adding
a 4th hot-path replica lets the anti-affinity rule place one pod
per physical host across node1/2/3/5 and gives the hourly f00-f18
grid chain 4-way parallel fan-out, with the NVMe-backed pod
handling its share of grib extraction.
2026-04-19 14:37:52 -05:00
FluxCD
3be5480af6 chore: update prop image to git.mcintire.me/graham/prop:main-1776626172-1b2424a [skip ci] 2026-04-19 19:19:50 +00:00
1b2424a1c1
feat(infra): add prop-backfill deployment for T620 nodes
PROP_ROLE=backfill drops the hot-path propagation/commercial/solar
queues and the Cron plugin. Paired with a dedicated Deployment
(nodeSelector: prop-backfill=true, 4Gi memory, no web) this lets 8GB
T620 workers drain the hrrr/weather/terrain/iemre/narr backfill
queues without competing with the hourly grid chain.
2026-04-19 14:14:28 -05:00
FluxCD
bc153055c6 chore: update prop image to git.mcintire.me/graham/prop:main-1776620433-6f63f3b [skip ci] 2026-04-19 17:43:07 +00:00
FluxCD
0942d0bbfb chore: update prop image to git.mcintire.me/graham/prop:main-1776620227-a6a3d3c [skip ci] 2026-04-19 17:40:04 +00:00
FluxCD
c38f4e2bfe chore: update prop image to git.mcintire.me/graham/prop:main-1776619974-9db9e6a [skip ci] 2026-04-19 17:35:46 +00:00
FluxCD
cad4bfc6f5 chore: update prop image to git.mcintire.me/graham/prop:main-1776619000-ffb14cb [skip ci] 2026-04-19 17:19:36 +00:00
FluxCD
d3a1e93587 chore: update prop image to git.mcintire.me/graham/prop:main-1776618115-01b181b [skip ci] 2026-04-19 17:04:32 +00:00
FluxCD
95a769bf0e chore: update prop image to git.mcintire.me/graham/prop:main-1776612418-0d02239 [skip ci] 2026-04-19 15:30:08 +00:00
FluxCD
543215864c chore: update prop image to git.mcintire.me/graham/prop:main-1776609556-f122eed [skip ci] 2026-04-19 14:41:49 +00:00
FluxCD
e41f84320a chore: update prop image to git.mcintire.me/graham/prop:main-1776608794-5cfb9e6 [skip ci] 2026-04-19 14:28:47 +00:00
FluxCD
ec3251c02f chore: update prop image to git.mcintire.me/graham/prop:main-1776607569-bbf6481 [skip ci] 2026-04-19 14:08:43 +00:00
FluxCD
3ab71cf8fe chore: update prop image to git.mcintire.me/graham/prop:main-1776606930-bb6333b [skip ci] 2026-04-19 13:57:42 +00:00
FluxCD
0e2d04f6d3 chore: update prop image to git.mcintire.me/graham/prop:main-1776606797-160439d [skip ci] 2026-04-19 13:55:40 +00:00
FluxCD
a8e225a92c chore: update prop image to git.mcintire.me/graham/prop:main-1776606153-504f514 [skip ci] 2026-04-19 13:44:38 +00:00
FluxCD
fe0bdd32c2 chore: update prop image to git.mcintire.me/graham/prop:main-1776605224-70e65ba [skip ci] 2026-04-19 13:29:36 +00:00
FluxCD
240bc30df8 chore: update prop image to git.mcintire.me/graham/prop:main-1776604171-28a9eb3 [skip ci] 2026-04-19 13:11:34 +00:00
FluxCD
69f4c81899 chore: update prop image to git.mcintire.me/graham/prop:main-1776551705-4e6c87e [skip ci] 2026-04-18 22:37:31 +00:00