Infra and test hygiene: CI gates, k8s probes, fixture/env-restore fixes #26

Merged
graham merged 1 commit from chore/infra-test-hygiene into main 2026-09-22 13:00:35 -05:00
Owner

Audit config/infra/tests group.

CI/infra:

  • build.yaml now runs the full precommit gate set (format-check, deps.unlock --check-unused, credo --strict, xref, test --warnings-as-errors); dead EXLA scaffolding removed.
  • build-grid-rs.yaml triggers on Elixir scorer/band-weights/golden changes too.
  • Migration 20260920160342 made safe under concurrent pod boots.
  • Liveness probes on both Rust worker deployments; secret.example.yaml synced; unused :exports queue dropped; config.exs queue counts aligned to prod; root working docs moved to docs/; .gitignore deduped + .serena/.

Tests:

  • Hardcoded W5TEST/test@example.com and copy-pasted contact dedup keys -> unique fixtures / shared ContactsFixtures.
  • 27 files' truthiness env restore -> fetch_env/case (a legitimately-false config value was being deleted).

Verified: all touched test files pass; full suite green on combined tree.

Audit config/infra/tests group. CI/infra: - build.yaml now runs the full precommit gate set (format-check, deps.unlock --check-unused, credo --strict, xref, test --warnings-as-errors); dead EXLA scaffolding removed. - build-grid-rs.yaml triggers on Elixir scorer/band-weights/golden changes too. - Migration 20260920160342 made safe under concurrent pod boots. - Liveness probes on both Rust worker deployments; secret.example.yaml synced; unused :exports queue dropped; config.exs queue counts aligned to prod; root working docs moved to docs/; .gitignore deduped + .serena/. Tests: - Hardcoded W5TEST/test@example.com and copy-pasted contact dedup keys -> unique fixtures / shared ContactsFixtures. - 27 files' truthiness env restore -> fetch_env/case (a legitimately-false config value was being deleted). Verified: all touched test files pass; full suite green on combined tree.
Infra and test hygiene: CI gates, k8s probes, fixture/env-restore fixes
Some checks failed
skippy-bot/review Skippy review: 1 blocking finding open — see the PR thread
420bf907d3
CI/infra:
- build.yaml now runs format-check, deps.unlock --check-unused,
  credo --strict, xref fail-above-4, and test --warnings-as-errors;
  dead EXLA scaffolding removed
- build-grid-rs.yaml also triggers on Elixir scorer/band-weights/golden
  changes so parity can't silently drift
- Migration 20260920160342 made safe under concurrent pod boots
- Liveness probes (/health:9100) on prop-grid-rs and hrrr-point-rs
- secret.example.yaml synced with runtime.exs env reads
- Unused :exports queue dropped; config.exs queue counts aligned to prod
- Root working docs moved to docs/plans/, tail_logs.sh to scripts/;
  .gitignore deduped and .serena/ ignored

Tests:
- Hardcoded W5TEST/test@example.com and copy-pasted contact dedup keys
  replaced with unique-integer fixtures / shared ContactsFixtures
- 27 files' truthiness env restore converted to fetch_env/case so a
  legitimately-false config value is restored, not deleted
skippy-bot left a comment

🤖 Skippy PR review

2 findings — 1 blocking before merge.

Severity Location Issue
🟠 High priv/repo/migrations/20260920160342_replace_hrrr_profiles_null_surface_refractivity_index.exs:97 Advisory lock is not held on a stable session, so it can leak and block a booting pod
🔵 Suggestion config/dev.exs:122 start_notify_listener is read by nothing, so it does not enable anything in dev

Reviewed 420bf907d34d. Comment skippy review to re-run.

### 🤖 Skippy PR review **2 findings** — 1 blocking before merge. | Severity | Location | Issue | | --- | --- | --- | | 🟠 High | `priv/repo/migrations/20260920160342_replace_hrrr_profiles_null_surface_refractivity_index.exs:97` | Advisory lock is not held on a stable session, so it can leak and block a booting pod | | 🔵 Suggestion | `config/dev.exs:122` | `start_notify_listener` is read by nothing, so it does not enable anything in dev | <sub>Reviewed `420bf907d34d`. Comment `skippy review` to re-run.</sub> <!-- skippy-pr-review -->
config/dev.exs Outdated
@ -120,2 +119,4 @@
config :microwaveprop, start_freshness_monitor: true
# Rust pipeline NOTIFY consumer (ScoreCache invalidation + /map refresh)
config :microwaveprop, start_notify_listener: true
First-time contributor

🔵 Suggestion — start_notify_listener is read by nothing, so it does not enable anything in dev

This is the only occurrence of :start_notify_listener in the repo: no Application.get_env/fetch_env call reads it, and Microwaveprop.Propagation.NotifyListener is not in the supervision tree either (its only lib/ mentions are comments, plus a test module). So the line changes nothing in dev, and the NotifyListener does not start. Either land the reader and the child spec together with this flag, or drop the key so the config does not read as if the consumer were off by default.

**🔵 Suggestion — `start_notify_listener` is read by nothing, so it does not enable anything in dev** This is the only occurrence of `:start_notify_listener` in the repo: no `Application.get_env`/`fetch_env` call reads it, and `Microwaveprop.Propagation.NotifyListener` is not in the supervision tree either (its only `lib/` mentions are comments, plus a test module). So the line changes nothing in dev, and the NotifyListener does not start. Either land the reader and the child spec together with this flag, or drop the key so the config does not read as if the consumer were off by default.
skippy-bot marked this conversation as resolved
@ -64,0 +94,4 @@
end
defp lock! do
execute "SELECT pg_advisory_lock(#{@advisory_lock_key})"
First-time contributor

🟠 High — Advisory lock is not held on a stable session, so it can leak and block a booting pod

@disable_ddl_transaction true means no transaction pins a connection for this migration, and @disable_migration_lock true also skips the checkout wrapper Ecto puts around its own advisory-lock strategy. Each execute therefore runs on whichever pool connection it happens to check out, so lock!() takes the lock on one connection, the CONCURRENTLY builds below run on others, and pg_advisory_unlock in the after block can land on a connection that never held it: it returns false, execute/1 discards the result, and the lock stays held by that idle pooled connection for the life of the pod.

Concretely: pod A takes the lock on connection C1, unlocks on Cn (no-op), and C1 keeps holding it. Pod B, booting in that window, blocks in its own pg_advisory_lock forever (the runner executes raw commands with timeout: :infinity), pinned on a pool slot, and its all: true run never reaches later migrations. So the lock provides accidental, leaky mutual exclusion instead of what the comment claims.

Ecto has this exact case covered: set migration_lock: :pg_advisory_lock on Microwaveprop.Repo and delete both @disable_migration_lock true and the hand-rolled lock!/unlock helpers. That strategy checks out one connection for the whole migration (so the lock and the CONCURRENTLY builds share a session) and uses pg_try_advisory_lock with retry rather than blocking forever. If you keep the hand-rolled version, pin a session explicitly, e.g. wrap up/0 and down/0 bodies in Ecto.Adapters.SQL.checkout(Ecto.Adapter.lookup_meta(repo()), [], fn -> ... end).

**🟠 High — Advisory lock is not held on a stable session, so it can leak and block a booting pod** `@disable_ddl_transaction true` means no transaction pins a connection for this migration, and `@disable_migration_lock true` also skips the `checkout` wrapper Ecto puts around its own advisory-lock strategy. Each `execute` therefore runs on whichever pool connection it happens to check out, so `lock!()` takes the lock on one connection, the `CONCURRENTLY` builds below run on others, and `pg_advisory_unlock` in the `after` block can land on a connection that never held it: it returns false, `execute/1` discards the result, and the lock stays held by that idle pooled connection for the life of the pod. Concretely: pod A takes the lock on connection C1, unlocks on Cn (no-op), and C1 keeps holding it. Pod B, booting in that window, blocks in its own `pg_advisory_lock` forever (the runner executes raw commands with `timeout: :infinity`), pinned on a pool slot, and its `all: true` run never reaches later migrations. So the lock provides accidental, leaky mutual exclusion instead of what the comment claims. Ecto has this exact case covered: set `migration_lock: :pg_advisory_lock` on `Microwaveprop.Repo` and delete both `@disable_migration_lock true` and the hand-rolled `lock!/unlock` helpers. That strategy checks out one connection for the whole migration (so the lock and the `CONCURRENTLY` builds share a session) and uses `pg_try_advisory_lock` with retry rather than blocking forever. If you keep the hand-rolled version, pin a session explicitly, e.g. wrap `up/0` and `down/0` bodies in `Ecto.Adapters.SQL.checkout(Ecto.Adapter.lookup_meta(repo()), [], fn -> ... end)`.
skippy-bot marked this conversation as resolved
graham force-pushed chore/infra-test-hygiene from 420bf907d3
Some checks failed
skippy-bot/review Skippy review: 1 blocking finding open — see the PR thread
to 33864f4fa1
Some checks failed
skippy-bot/review Skippy review: 2 blocking findings open — see the PR thread
2026-09-22 12:04:18 -05:00
Compare
skippy-bot left a comment

🤖 Skippy PR review

1 finding — 1 blocking before merge.

Severity Location Issue
🟠 High config/runtime.exs:263 Dropping the :exports queue silently kills the CSV/PDF export feature in prod

Branch was rebased (420bf907d34d is no longer in the PR history), so this pass covers the full diff and only adds what was not already posted. Resolved the earlier start_notify_listener suggestion: NotifyListener is now in the supervision tree and reads the flag, so the dev key is no longer dead. 1 earlier finding still open: the migration's hand-rolled advisory lock.

Reviewed 33864f4fa15a. Comment skippy review to re-run.

### 🤖 Skippy PR review **1 finding** — 1 blocking before merge. | Severity | Location | Issue | | --- | --- | --- | | 🟠 High | `config/runtime.exs:263` | Dropping the `:exports` queue silently kills the CSV/PDF export feature in prod | Branch was rebased (`420bf907d34d` is no longer in the PR history), so this pass covers the full diff and only adds what was not already posted. Resolved the earlier `start_notify_listener` suggestion: `NotifyListener` is now in the supervision tree and reads the flag, so the dev key is no longer dead. 1 earlier finding still open: the migration's hand-rolled advisory lock. <sub>Reviewed `33864f4fa15a`. Comment `skippy review` to re-run.</sub> <!-- skippy-pr-review -->
@ -261,4 +261,3 @@
space_weather: 1,
mechanism: 4,
exports: 1,
gefs: 1
First-time contributor

🟠 High — Dropping the :exports queue silently kills the CSV/PDF export feature in prod

The :exports queue is not unused: it is the queue live_table's only two workers declare (use Oban.Worker, queue: :exports in LiveTable.Workers.CsvExportWorker / PdfExportWorker), and LiveTable.ExportHelpers.handle_event("export-csv"/"export-pdf") inserts them on every Export click. config/config.exs never declared exports, so with the key removed here (and in config/dev.exs) nothing declares the queue anywhere and no pod starts a producer for it, on either PROP_ROLE.

Oban validates only the queue name's length at insert time (deps/oban/lib/oban/job.ex:334), not that the queue is configured, so {:ok, _job} = ... |> Oban.insert() still succeeds and the job sits in available forever. MicrowavepropWeb.LiveTableResource subscribes to exports:<client_id> and only reacts to {:file_ready, path}, which only the (never-running) worker broadcasts, so the button does nothing visible: /contacts (its table sets exports: %{formats: [:csv]}) plus every table taking live_table's default exports: %{enabled: true} (/beacons, /users, /admin/contact-edits, the rover tables). Meanwhile available rows pile up in oban_jobs.

Either restore exports: 1 in shared_queues (and in config/dev.exs), or drop the export affordance together with its plumbing: MicrowavepropWeb.ExportStore, ExportController and the /exports/:token route exist only to serve these jobs.

**🟠 High — Dropping the `:exports` queue silently kills the CSV/PDF export feature in prod** The `:exports` queue is not unused: it is the queue `live_table`'s only two workers declare (`use Oban.Worker, queue: :exports` in `LiveTable.Workers.CsvExportWorker` / `PdfExportWorker`), and `LiveTable.ExportHelpers.handle_event("export-csv"/"export-pdf")` inserts them on every Export click. `config/config.exs` never declared `exports`, so with the key removed here (and in `config/dev.exs`) nothing declares the queue anywhere and no pod starts a producer for it, on either `PROP_ROLE`. Oban validates only the queue name's *length* at insert time (`deps/oban/lib/oban/job.ex:334`), not that the queue is configured, so `{:ok, _job} = ... |> Oban.insert()` still succeeds and the job sits in `available` forever. `MicrowavepropWeb.LiveTableResource` subscribes to `exports:<client_id>` and only reacts to `{:file_ready, path}`, which only the (never-running) worker broadcasts, so the button does nothing visible: `/contacts` (its table sets `exports: %{formats: [:csv]}`) plus every table taking live_table's default `exports: %{enabled: true}` (/beacons, /users, /admin/contact-edits, the rover tables). Meanwhile `available` rows pile up in `oban_jobs`. Either restore `exports: 1` in `shared_queues` (and in `config/dev.exs`), or drop the export affordance together with its plumbing: `MicrowavepropWeb.ExportStore`, `ExportController` and the `/exports/:token` route exist only to serve these jobs.
Author
Owner

Addressed in 166787c9:

  1. Advisory lock leaks across pooled connectionsup/0 and down/0 now run inside repo().checkout/1, so pg_advisory_lock, the CONCURRENTLY builds, and pg_advisory_unlock all share one session. (Kept the hand-rolled lock rather than a repo-wide migration_lock change so the strategy stays scoped to this migration.)
  2. start_notify_listener unread — stale: notify_listener.ex:40 reads the key and application.ex supervises the listener since #18 merged. The dev flag was still redundant (the default is true), so it's dropped.
Addressed in 166787c9: 1. **Advisory lock leaks across pooled connections** — `up/0` and `down/0` now run inside `repo().checkout/1`, so `pg_advisory_lock`, the CONCURRENTLY builds, and `pg_advisory_unlock` all share one session. (Kept the hand-rolled lock rather than a repo-wide `migration_lock` change so the strategy stays scoped to this migration.) 2. **`start_notify_listener` unread** — stale: `notify_listener.ex:40` reads the key and `application.ex` supervises the listener since #18 merged. The dev flag was still redundant (the default is `true`), so it's dropped.
graham force-pushed chore/infra-test-hygiene from 33864f4fa1
Some checks failed
skippy-bot/review Skippy review: 2 blocking findings open — see the PR thread
to 166787c9fc
Some checks failed
skippy-bot/review Skippy review: 2 blocking findings open — see the PR thread
2026-09-22 13:00:13 -05:00
Compare
graham merged commit beb52529b4 into main 2026-09-22 13:00:35 -05:00
skippy-bot left a comment

🤖 Skippy PR review

1 finding — 1 blocking before merge.

Severity Location Issue
🟠 High CHANGELOG.md:34 Merge conflict markers are committed in CHANGELOG.md

Branch was force-pushed (33864f4fa1 is no longer in the PR history), so this pass covers the full diff and only adds what was not already posted. Resolved the advisory-lock finding: repo().checkout/1 now pins the lock, the CONCURRENTLY builds and the unlock to a single pooled session. 1 earlier finding still open (the dropped :exports queue). The PR was merged into main as beb52529 while this review ran, so both blockers are now live on main.

Reviewed 166787c9fca4. Comment skippy review to re-run.

### 🤖 Skippy PR review **1 finding** — 1 blocking before merge. | Severity | Location | Issue | | --- | --- | --- | | 🟠 High | `CHANGELOG.md:34` | Merge conflict markers are committed in CHANGELOG.md | Branch was force-pushed (33864f4fa15a is no longer in the PR history), so this pass covers the full diff and only adds what was not already posted. Resolved the advisory-lock finding: repo().checkout/1 now pins the lock, the CONCURRENTLY builds and the unlock to a single pooled session. 1 earlier finding still open (the dropped :exports queue). The PR was merged into main as beb52529 while this review ran, so both blockers are now live on main. <sub>Reviewed `166787c9fca4`. Comment `skippy review` to re-run.</sub> <!-- skippy-pr-review -->
@ -31,6 +31,7 @@ defaults to the current hour rather than the furthest-future forecast.
Path-averaged wind no longer mixes components from different grid cells when
one is missing, and the scoring recalibrator now trains on the same
missing-data rules the live scorer uses.
<<<<<<< HEAD
First-time contributor

🟠 High — Merge conflict markers are committed in CHANGELOG.md

Lines 34, 77 and 83 are raw conflict markers (<<<<<<< HEAD, =======, >>>>>>> 33864f4f (Infra and test hygiene: ...)), so the rebase conflict was committed unresolved. Microwaveprop.Changelog parses this file at compile time and /changelog renders it to visitors, so the marker lines show up as body prose of the entries they land in (the <<<<<<< line in "More accurate path wind and recalibration inputs", ======= in "Forecast pipeline reliability fixes", >>>>>>> in the new "Infrastructure and reliability cleanup" entry). It does not crash the compile: the parser only splits on ^## / ^### , and >>>>>>> 33864f4f (...) is not a heading, so this ships silently and is now on main (merge beb52529). Fix: delete lines 34, 77 and 83, keep the infra entry, recompile. This is the only file in the tree with markers.

**🟠 High — Merge conflict markers are committed in CHANGELOG.md** Lines 34, 77 and 83 are raw conflict markers (`<<<<<<< HEAD`, `=======`, `>>>>>>> 33864f4f (Infra and test hygiene: ...)`), so the rebase conflict was committed unresolved. `Microwaveprop.Changelog` parses this file at compile time and `/changelog` renders it to visitors, so the marker lines show up as body prose of the entries they land in (the `<<<<<<<` line in "More accurate path wind and recalibration inputs", `=======` in "Forecast pipeline reliability fixes", `>>>>>>>` in the new "Infrastructure and reliability cleanup" entry). It does not crash the compile: the parser only splits on `^## ` / `^### `, and `>>>>>>> 33864f4f (...)` is not a heading, so this ships silently and is now on main (merge `beb52529`). Fix: delete lines 34, 77 and 83, keep the infra entry, recompile. This is the only file in the tree with markers.
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
graham/prop!26
No description provided.