Adds HorizontalPodAutoscaler resources for the three load-responsive Deployments: - prop (web): min 2, max 5, averageValue 800m CPU - prop-grid-rs: min 1, max 4, averageValue 400m - hrrr-point-rs: min 1, max 4, averageValue 400m - prop-backfill is intentionally unmanaged (queue-driven, not CPU-bound) The scale-up policy reacts in 30 s with up-to-2-pods or +100 % per window; scale-down stabilises over 10 min so the hourly propagation chain doesn't churn replicas right after the :05 cron. Removes `spec.replicas` from the HPA-managed Deployments so Flux's periodic reconcile stops fighting HPA's live scaling decisions. Drops `podAntiAffinity` from prop, prop-grid-rs, and hrrr-point-rs so HPA can actually reach its maxReplicas when one physical host has capacity for two pods; the scheduler picks whatever fits. Updates runbook + project memory to drop the `talos5` hostname — Postgres is now addressed strictly by IP (10.0.15.30).
5.3 KiB
Propagation pipeline runbook
The hourly propagation grid flows across two runtimes and a shared NFS volume. This doc names every failure mode at that seam and says how the system recovers.
prop-grid-rs (worker nodes) Postgres (10.0.15.30) prop pods (node1/2/3)
├─ fetch GRIB2 (NOMADS) ├─ grid_tasks (SKIP LOCKED) ├─ ScoreCache (ETS)
├─ decode ├─ LISTEN/NOTIFY channel ├─ NotifyListener
├─ score 22 bands × 92k │ "propagation_ready" ├─ ScoreCacheReconciler (60s)
├─ write /data/scores/.../.prop │ └─ PubSub → LiveView
└─ NOTIFY propagation_ready ────┘
Primary refresh path (happy case)
- Rust completes
{band, valid_time}compute and writes the.propfile via atomic rename. (Readers also accept legacy.ntmsfiles withNTMSmagic during a rolling deploy.) - Rust emits
NOTIFY propagation_ready '<iso valid_time>'. NotifyListeneron each Elixir pod receives it and callsPropagation.warm_cache_and_broadcast/2which reads the file once and PubSub-broadcasts the payload to every peer.- Each pod's
ScoreCachereceives the message and writes into its local ETS table./mapclients are notified via the"propagation:updated"topic.
Latency budget end-to-end: < 2s.
Failure modes
FM1 — NOTIFY dropped on reconnect
Symptom. {band, valid_time} files land on disk but ScoreCache
stays cold until the first lazy miss.
Why. Postgres queues LISTEN notifications per-connection and discards any that are unread on disconnect. A DB restart, a network blip, or the Postgrex.Notifications connection hitting its idle timeout all drop whatever queued notifies were in flight.
Recovery. ScoreCacheReconciler sweeps /data/scores every 60s
per-pod and warms missing {band, valid_time} pairs from disk into
local ETS. Each pod reconciles independently — no cluster messaging
required.
FM2 — NotifyListener not running locally
Symptom. One pod is cold while peers are warm. /map load time
on that pod is ~150ms slower than average until the reconciler runs.
Why. The listener isn't currently part of the supervision tree
(see lib/microwaveprop/application.ex). When it is re-added, early
startup or Postgrex.Notifications boot failure can leave it
unsubscribed for several seconds while Rust writes land.
Recovery. Same as FM1 — reconciler catches up within one tick.
FM3 — NFS stale read racing Rust write
Symptom. ScoresFile.read/2 returns {:error, reason} when
the reader opens the file exactly between Rust's rename(2) of the
.tmp.<uniq> file and the fsync landing on the NFS server.
Why. The rename is atomic on the same NFSv4 filesystem, but the NFS client cache can briefly serve a stale directory listing or a no-longer-existing path.
Recovery. Reconciler calls ScoresFile.read/2 directly and
pattern-matches {:error, _} -> :error, logging at debug and
skipping the key. The next tick re-reads the file after NFS
consistency settles. Lazy-miss path
(Propagation.scores_at_fetch/3) has historically surfaced this as
a LiveView error — the reconciler warms the cache before that miss
happens.
FM4 — Cluster partition splits ScoreCache broadcast
Symptom. Pods on one side of a cluster partition have up-to-date cache; pods on the other side don't, even though both can read NFS and Postgres.
Why. ScoreCache.broadcast_put/3 fans the payload out over
PubSub. A libcluster partition stops the message reaching partitioned
peers.
Recovery. Each pod's reconciler reads from NFS directly, so it converges regardless of PubSub reachability. Partition does not cause divergent scores — only transient latency until the next sweep.
FM5 — Rust worker dead
Symptom. grid_tasks rows with status = 'queued' accumulate;
no new .prop files land; /map serves the last successful run's
scores.
Why. Deployment bug, Rust panic, OOM on a worker node, or
disk-full on /data.
Recovery. Manual. kubectl -n prop logs deploy/prop-grid-rs for
cause. The Elixir side is read-only against the grid — it does not
regenerate scores from within the BEAM since the extraction in
65693ed. lib/microwaveprop/workers/propagation_grid_worker.ex
still exists as a seed worker: the hourly cron fires it with empty
args and it inserts 19 grid_tasks rows (f00 + f01..f18) for Rust
to drain. All fetch/decode/score helpers moved to
rust/prop_grid_rs/src/pipeline.rs; the Elixir module is retained
only as the cron entry point, not as a fallback compute path.
Monitoring signals
avg_over_time(beam_memory_total_bytes[1h])on prop pods — steady climb above 1 GiB means the cache isn't pruning (seeScoreCache.prune_older_than/1inNotifyListener).- Reconciler log line
"ScoreCacheReconciler: warmed N"— if N is consistently non-zero every minute, NOTIFY is dropping and NotifyListener wants investigation. grid_tasksqueue depth in Prometheus — should drain to 0 between hourly cron ticks.
Test coverage
test/microwaveprop/propagation/score_cache_reconciler_test.exscovers FM1 and FM3 (missing-file skip).- Partition recovery (FM4) is untested in CI — requires multi-node cluster and libcluster orchestration.