prop/k8s/deployment-grid-rs.yaml
Graham McIntire 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

153 lines
5.7 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

apiVersion: apps/v1
kind: Deployment
metadata:
name: prop-grid-rs
namespace: prop
# Phase 2 cutover: Rust writes directly to /data/scores. Elixir only
# runs the f00 analysis-hour step (which carries enrichment Rust
# doesn't own yet — native-level duct merge, NEXRAD composite,
# commercial-link degradation, ProfilesFile write). f01..f18 land
# here from the Rust worker.
spec:
# Two replicas for HA. FOR UPDATE SKIP LOCKED lets both pods claim
# from grid_tasks without coordination. Anti-affinity spreads them
# across hosts so talos5 going down doesn't halt the hourly chain.
replicas: 2
minReadySeconds: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 0
maxUnavailable: 1
selector:
matchLabels:
app: prop-grid-rs
template:
metadata:
labels:
app: prop-grid-rs
tier: grid-rs
annotations:
# Scraped by the Prometheus at 10.0.15.25. Metrics endpoint is
# on container port 9100 (see METRICS_ADDR env below).
prometheus.io/scrape: "true"
prometheus.io/port: "9100"
prometheus.io/path: "/metrics"
spec:
# Primary replica prefers talos5 (32 GB NUC with NVMe). Secondary
# replica lands anywhere else the scheduler allows — the
# required anti-affinity rule below guarantees the two don't
# stack on the same host.
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: prop-grid-rs
topologyKey: kubernetes.io/hostname
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: prop-grid-rs
operator: In
values: ["primary"]
tolerations:
- key: workload
operator: Equal
value: grid-rs
effect: NoSchedule
imagePullSecrets:
- name: forgejo-registry
securityContext:
runAsUser: 65534
runAsNonRoot: true
fsGroup: 65534
seccompProfile:
type: RuntimeDefault
containers:
- name: prop-grid-rs
# Image built by a new CI pipeline from rust/prop_grid_rs/.
# Multi-arch (amd64 + arm64) — reuses the repo's existing
# buildx wiring.
image: git.mcintire.me/graham/prop-grid-rs:main-1776640915-65f7963 # {"$imagepolicy": "flux-system:prop-grid-rs"}
imagePullPolicy: IfNotPresent
env:
# Elixir secrets bundle carries DATABASE_URL already. Rust
# reads it directly (sqlx connects with the same URL).
- name: HRRR_BASE_URL
value: "http://skippy.w5isp.com:8080"
- name: PROP_SCORES_DIR
value: "/data/scores"
- name: RUST_LOG
value: "info"
# talos5 is 4c/4t (i5-7260U). Each replica claims up to this
# many forecast hours in parallel. With 2 replicas × 3 → 6
# concurrent tasks, the 18-hour chain completes in ~3 min.
# Per-task peak RAM is ~250 Mi post-streaming refactor, so
# 3×250 ≈ 750 Mi per pod, well under the 2 Gi limit. The
# secondary replica may land on a smaller node — 3 keeps it
# honest.
- name: PROP_GRID_RS_PARALLELISM
value: "3"
# Pool size = parallelism + 2 (NOTIFY + retry slack). Kept in
# sync with PROP_GRID_RS_PARALLELISM when either changes.
- name: PROP_GRID_RS_PG_CONNS
value: "5"
- name: METRICS_ADDR
value: "0.0.0.0:9100"
envFrom:
- secretRef:
name: prop-secrets
ports:
- name: metrics
containerPort: 9100
protocol: TCP
# Liveness is "the process is still running"; let the kubelet
# restart us if we crash. Claiming work from grid_tasks is the
# implicit readiness signal — an idle pod is still healthy
# (the queue is simply empty). A minimal readiness check on
# /health confirms the metrics listener is up, which doubles
# as evidence the tokio runtime is alive.
readinessProbe:
httpGet:
path: /health
port: 9100
# Generous initialDelaySeconds so a slow DB connect during
# startup (Postgres acquire_timeout is 10s) can't race the
# first probe. failureThreshold=6 keeps the pod from
# flapping if a single scrape times out during a long
# chain step.
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 6
resources:
requests:
cpu: 100m
memory: 512Mi
limits:
cpu: "4"
# OOM at 1 Gi during active chain work (92k points × 23
# bands ≈ 2.1M scores, plus decoded GRIB2 buffers, plus
# wgrib2 subprocess memory). 2 Gi leaves headroom; we're
# still an order of magnitude below the 6 Gi Elixir budget.
memory: 2Gi
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 65534
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
nfs:
server: 10.0.15.103
path: /data