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-1776639595-1307b1d # {"$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 initialDelaySeconds: 3 periodSeconds: 10 timeoutSeconds: 2 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