prop/lib/microwaveprop/workers/propagation_grid_worker.ex
Graham McIntire 063e9e3ae4
fix(grid-tasks): reclaim orphan running rows on hourly seed
Rust workers (prop-grid-rs) that die mid-claim (SIGKILL, OOM, node
drain) leave grid_tasks rows stuck in status='running' forever,
because claim_next uses FOR UPDATE SKIP LOCKED and nothing resets the
orphan. The /status page then shows a permanent spinner with stale
f00/f10/f18 badges — currently 21 rows claimed as far back as
2026-04-20.

GridTaskEnqueuer.reclaim_stale_running/1 flips rows whose claimed_at
is older than 15 minutes back to 'queued'. Rows that have already
burned through 5 claim/reclaim cycles become 'failed' with a
reclaim-orphan error so the next hourly seed can replace them.
Wired into PropagationGridWorker.seed_chain/0 so it runs every :05
cron tick before new rows are seeded.

Also rename the status panel "Retrying" column to "Failed" — it was
always showing terminal `failed` rows, never retrying ones.
2026-04-23 12:57:39 -05:00

75 lines
2.7 KiB
Elixir

defmodule Microwaveprop.Workers.PropagationGridWorker do
@moduledoc """
Hourly seed worker for the Rust `prop-grid-rs` chain.
Post-Phase-3-cutover, Elixir no longer runs any fetch/decode/score
work for the propagation grid. The hourly cron fires this worker
with empty args, and it inserts 19 `grid_tasks` rows (1 analysis
f00 + 18 forecast f01..f18) for Rust to drain. Rust owns everything
from there: HRRR fetch, wgrib2 decode, native-level duct merge,
NEXRAD composite, commercial-link degradation, band scoring, and
both the ProfilesFile (MessagePack) and the per-band score files.
The old chain-step perform/2 clauses and the merge/score helpers
moved to `rust/prop_grid_rs/src/pipeline.rs`. `Propagation.record_run_timing`
is still called from this module when the Rust worker reports a
chain step, through the PropagationNotifyListener path.
"""
use Oban.Worker,
queue: :propagation,
priority: 0,
max_attempts: 5,
# Deduplicate seed jobs in a 1-hour window. The hourly cron fires
# `%{}` args; unique protects against FreshnessMonitor re-fires
# during an outage stacking multiple chains for the same run.
unique: [period: 3600, states: [:available, :scheduled, :executing, :retryable]]
alias Microwaveprop.Instrument
alias Microwaveprop.Propagation.GridTaskEnqueuer
alias Microwaveprop.Weather.HrrrClient
require Logger
@impl Oban.Worker
def perform(%Oban.Job{args: args}) when args == %{} do
Instrument.span([:propagation, :grid_worker, :perform], %{}, fn ->
seed_chain()
end)
end
defp seed_chain do
# Reclaim any `running` grid_tasks rows orphaned by a Rust worker
# that died mid-claim (SIGKILL, OOM, node drain). Without this
# the status panel shows a permanent-looking spinner and the
# rows are never re-worked.
_ = GridTaskEnqueuer.reclaim_stale_running()
# HRRR takes ~45min to publish after the hour. Use 2 hours ago to
# ensure availability.
two_hours_ago = DateTime.add(DateTime.utc_now(), -2, :hour)
run_time = two_hours_ago |> HrrrClient.nearest_hrrr_hour() |> DateTime.truncate(:second)
Logger.info("PropagationGrid: seeding chain run_time=#{run_time} (f00 analysis + f01-f18 forecasts via grid_tasks)")
case GridTaskEnqueuer.seed_with_analysis(run_time) do
{:ok, count} ->
:telemetry.execute(
[:microwaveprop, :propagation, :grid_worker, :seeded],
%{queued_tasks: count},
%{run_time: run_time}
)
:ok
{:error, reason} ->
:telemetry.execute(
[:microwaveprop, :propagation, :grid_worker, :exception],
%{},
%{run_time: run_time, reason: inspect(reason)}
)
{:error, reason}
end
end
end