Fix propagation pipeline bugs: supervise NotifyListener, harden score reads #18
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/propagation-pipeline"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fixes from the 2026-09-22 audit (tmp/bugs.md), propagation core group.
Verified: full suite 5000 tests pass on the combined tree; targeted tests green.
🤖 Skippy PR review
2 findings — 2 blocking before merge.
lib/microwaveprop/propagation/notify_listener.ex:88lib/microwaveprop/propagation/notify_listener.ex:109Reviewed
233850c266df. Commentskippy reviewto re-run.@ -72,3 +86,3 @@{:ok, run_time, _} <- DateTime.from_iso8601(run_time_str),{:ok, valid_time, _} <- DateTime.from_iso8601(valid_time_str) dohandle_propagation_ready(run_time, valid_time)source = List.first(rest) || "hrrr"🟠 High — The hrrr-only retain gate is inert: Rust never sends the source field
rust/prop_grid_rs/src/db.rscomplete()still formats the payload as"{}|{}"(run_time, valid_time) with no source, andgit diff main...HEAD -- rust/is empty, sorestis always[],sourceis always the"hrrr"fallback, and the sweep at line 194 still runs for every HRDPS completion exactly as before. The damage the PR set out to stop is therefore untouched:HrdpsGridWorker.pick_run_time/1seedsrun_time = now - 4h(ornow - 10h), soretain_scores_window/1anchors the 48 h profile/scalar window 4-10 h behind wall clock and deletes the live HRRR f39-f48 files.The default is also the destructive direction, so this re-arms itself any time the field goes missing (and
List.first(rest) || "hrrr"maps a present-but-empty field such asa|b|to"", which skips retention and leaks disk instead). Fix both ends: bindtask.sourceinto the payload indb.rs complete()(ClaimedTaskalready carries it), and until that ships make the absent/unknown case fail safe by skipping the sweep rather than defaulting to delete (the 15 minPropagationPruneWorkerstill bounds disk).The new test feeds a 3-field payload the producer never emits, so the suite cannot catch this.
@ -89,0 +106,4 @@# before the :DOWN/retry path below could ever run. On reconnect# Postgrex.Notifications re-issues LISTEN for every registered# channel, so no resubscribe is needed here.opts = Keyword.merge(config, auto_reconnect: true, reconnect_backoff: 5_000)🟠 High — Raw Repo config has no connection details in prod, so the listener never subscribes
Application.fetch_env!(:microwaveprop, Repo)(line 101) returns the unparsed config. In prod that is only[url: DATABASE_URL, pool_size: 12, socket_options: ...](config/runtime.exs:465) because Ecto itself turns:urlinto username/password/database/hostname (Ecto.Repo.Supervisor.init_config/4) and never writes the result back to the app env, while Postgrex has no:urlhandling at all (the stringurldoes not appear anywhere in postgrex 0.22.4'slib/).So
Postgrex.Notificationsgets none of the connection keys and falls back to the process-env defaults fromPostgrex.Utils.default_opts/1: localhost:5432, no database, no password. Nothing ink8s/deployment.yamlor the secret bundle setsPGHOST/PGUSER/PGPASSWORD/PGDATABASE, so that connect fails,SimpleConnectiondefaultssync_connect: true,start_linkreturns{:error, _}, and this GenServer logs "failed to subscribe" and retries every 30 s forever: the listener still never gets a LISTEN in production, which is the bug this PR exists to fix (and dev only works because config/dev.exs sets username/password/hostname/database directly).Use
Microwaveprop.Repo.config()instead, which is whatRepoListeneralready does to get the URL-parsed config, or pass hostname/port/username/password/database explicitly.Both findings addressed in
9336e7e3:Inert source gate — missing/empty source now skips the retain sweep entirely (fail-safe; PropagationPruneWorker still bounds disk). The emit side (
db.rscomplete()→run_time|valid_time|source) is in #25; until it merges, retention simply does not run rather than guessing wrong. Tests updated: 2-field payload asserts the file survives, 3-fieldhrrrasserts the sweep runs.Raw Repo config — now uses
Repo.config()(URL-parsed) viaRepoListener.start_notifications/2, which also carries the direct-5432 pgbouncer bypass.extra_optsparam added forauto_reconnect.skippy review
Resolved 2 of 2 earlier findings (both on
notify_listener.ex):"hrrr"— a missing field now yieldsniland skips the retain sweep, so a stale HRDPSrun_timecan't delete live HRRR f39-f48 files, and a present-but-empty field no longer silently skips into a leak.Repo.config()(URL-parsed hostname/username/password/database) through the sharedRepoListener.start_notifications/2, so prod no longer hands Postgrex a bareurl:config.Nothing new in
233850c266df..9336e7e3: the new source branch, theextra_optsmerge indo_start_notifications/4, and the three payload tests all read correctly.Not blocking, worth knowing:
rust/prop_grid_rs/src/db.rsstill emits the two-field payload, so the sweep stays dormant (disk stays bounded by the 15-minutePropagationPruneWorker, which prunes the same scores/profiles/scalars trees with a 3 h cutoff) — and the "retains GEFS score files through 168 hours" test now passes vacuously, since it callshandle_propagation_ready/2with the nil default.Resolved 2 of 2 earlier findings at
9336e7e3:Repoconfig (notify_listener.ex:116): nowRepo.config(), which Ecto 3.14.2 parses viainit_config/4+parse_url/1into hostname/username/password/database, so the prod listener gets a real connection instead of Postgrex defaults.notify_listener.ex:93): absent/empty source now yieldsniland skips the sweep, so a stale HRDPSrun_timecan no longer trim live HRRR f39-f48 files. Disk stays bounded by the 15-minutePropagationPruneWorker.Nothing new in
233850c2..9336e7e3.One residual, non-blocking:
rust/prop_grid_rs/src/db.rs:complete/2still formats the payload as"{}|{}", sosourceis never present andretain_scores_window/1is now dormant in production (only the past-dated prune runs, leaving GEFS/future files to their producer horizons). Bindtask.sourceinto the payload when the Rust side is next touched.