diff --git a/CLAUDE.md b/CLAUDE.md index e0ca2d02..77d12f55 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,6 +56,10 @@ mix credo # Pre-commit check (compile with warnings-as-errors, unlock unused deps, format, test) mix precommit +# Rust pre-commit (matches the Forgejo CI step). Run from rust/prop_grid_rs/. +# `cargo build` alone does NOT catch clippy errors that gate the CI build. +cd rust/prop_grid_rs && cargo clippy --all-targets -- -D warnings && cargo test --release + # Database mix ecto.create mix ecto.migrate @@ -211,6 +215,11 @@ User submits QSO → enqueue_for_qso() → weather/hrrr/terrain/iemre workers - Stub HTTP calls with `Req.Test.stub` in test setup - Oban runs inline in test (`testing: :inline`) - FreshnessMonitor disabled in test via `config :microwaveprop, start_freshness_monitor: false` +- **Don't anchor relative-time test fixtures on `DateTime.utc_now()` truncated to the hour.** A test that computes `recent_t = (top_of_hour) - 30min` is 30-90 min old of real `utc_now()` depending on what minute the test runs at; any production code that compares against `utc_now()` (e.g. a "drop entries older than 1 h" filter) flakes 50% of the time. Anchor at `top_of_hour` (or use a tiny offset like `now - 1s`) so the test is stable across clock minutes. + +### Rust (`prop_grid_rs`) +- **Always run `cargo clippy --all-targets -- -D warnings` before committing Rust changes.** The Forgejo CI step runs clippy with warnings-as-errors and a passing local `cargo build` does NOT catch lints (e.g. `manual_range_contains`, `iter_kv_map`, `useless_format`). A clippy failure blocks the image build and the deploy. The Rust precommit recipe is in the `Commands` section. +- The CI build pipeline runs `cargo clippy --all-targets -- -D warnings && cargo test --release && cargo build --release`. Match this locally — `cargo build` alone is not enough. ### Deployment - Production runs on Kubernetes (`home-cluster` context, `prop` namespace) — use `kubectl -n prop ...` diff --git a/k8s/hpa.yaml b/k8s/hpa.yaml index da229d7e..c5b86272 100644 --- a/k8s/hpa.yaml +++ b/k8s/hpa.yaml @@ -82,17 +82,26 @@ spec: averageValue: 400m behavior: scaleUp: + # Genuine surges (a stuck queue, multiple cycles draining together) + # still bring up replicas within ~30s. Most hourly chain steps + # finish in 2-3 min on a single pod, so transient single-pod + # spikes mostly resolve before HPA notices. stabilizationWindowSeconds: 30 policies: - type: Pods value: 1 periodSeconds: 30 scaleDown: - stabilizationWindowSeconds: 600 + # The previous 10-minute stabilization + 1-pod-per-2-min drain meant + # an hourly chain spike that briefly scaled to 4 pods sat at 4 pods + # for ~16 min after the spike cleared. Bring it down to 2 min + # stabilization + 100%-per-60s drain so the steady state is 1 pod + # within ~3 min of any spike ending. + stabilizationWindowSeconds: 120 policies: - - type: Pods - value: 1 - periodSeconds: 120 + - type: Percent + value: 100 + periodSeconds: 60 --- apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler @@ -115,14 +124,23 @@ spec: averageValue: 400m behavior: scaleUp: + # Genuine surges (a stuck queue, multiple cycles draining together) + # still bring up replicas within ~30s. Most hourly chain steps + # finish in 2-3 min on a single pod, so transient single-pod + # spikes mostly resolve before HPA notices. stabilizationWindowSeconds: 30 policies: - type: Pods value: 1 periodSeconds: 30 scaleDown: - stabilizationWindowSeconds: 600 + # The previous 10-minute stabilization + 1-pod-per-2-min drain meant + # an hourly chain spike that briefly scaled to 4 pods sat at 4 pods + # for ~16 min after the spike cleared. Bring it down to 2 min + # stabilization + 100%-per-60s drain so the steady state is 1 pod + # within ~3 min of any spike ending. + stabilizationWindowSeconds: 120 policies: - - type: Pods - value: 1 - periodSeconds: 120 + - type: Percent + value: 100 + periodSeconds: 60