prop/lib/microwaveprop/workers/hrdps_grid_worker.ex
2026-06-16 12:38:08 -05:00

98 lines
3.3 KiB
Elixir
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

defmodule Microwaveprop.Workers.HrdpsGridWorker do
@moduledoc """
Cycle seed worker for the HRDPS Canadian propagation chain. Mirrors
`Microwaveprop.Workers.PropagationGridWorker` but seeds `grid_tasks`
rows with `source: "hrdps"` so the Rust worker routes them through
the HRDPS fetch path.
## Cadence
HRDPS runs 4×/day at 00/06/12/18Z and publishes f000 ~3-4h after each
cycle. This worker is intended to fire at HH+5 of each cycle hour
(05:35Z, 11:35Z, 17:35Z, 23:35Z) — see plan stage 6 for the cron entry.
## Activation
Active. The Rust prop-grid-rs worker handles `source: "hrdps"` rows via
`pipeline::run_chain_step_hrdps`, fetching from MSC Datamart, writing
scores to `<base>/<band>/<iso>.hrdps.prop` (sibling of the HRRR
`.prop` file for the same cycle). Cron entry lives in `runtime.exs`.
## Scope
Seeds exactly one forecast row per fire — the one whose valid_time
matches the current hour. The Canadian propagation/weather UI only
surfaces the present moment, so producing forecast hours per cycle
was wasted wgrib2 work (~20 min/chain step at production point
density). If forecast hours come back into scope later, swap the
enqueuer call back to `GridTaskEnqueuer.seed_with_analysis/2`.
"""
use Oban.Pro.Worker,
queue: :hrdps,
priority: 0,
max_attempts: 5,
# Deduplicate seed jobs in a 6-hour window matching the cycle cadence.
unique: [period: 6 * 3600, states: :incomplete]
alias Microwaveprop.Instrument
alias Microwaveprop.Propagation.GridTaskEnqueuer
alias Microwaveprop.Weather.HrdpsClient
require Logger
@impl Oban.Pro.Worker
def process(%Oban.Job{args: args}) when args == %{} do
Instrument.span([:hrdps, :grid_worker, :perform], %{}, fn ->
seed_chain()
end)
end
defp seed_chain do
# HRDPS shares grid_tasks with HRRR; the same reclaim sweep covers
# both because reclaim_stale_running/0 doesn't filter on source.
_ = GridTaskEnqueuer.reclaim_stale_running()
run_time = pick_run_time(DateTime.utc_now())
Logger.info("HrdpsGrid: seeding current-hour task run_time=#{run_time} source=hrdps")
case GridTaskEnqueuer.seed_current_hour(run_time, source: "hrdps") do
{:ok, count} ->
:telemetry.execute(
[:microwaveprop, :hrdps, :grid_worker, :seeded],
%{queued_tasks: count},
%{run_time: run_time}
)
:ok
{:error, reason} ->
:telemetry.execute(
[:microwaveprop, :hrdps, :grid_worker, :exception],
%{},
%{run_time: run_time, reason: inspect(reason)}
)
{:error, reason}
end
end
@doc """
Pick the freshest published HRDPS cycle. Probes `now-4h` (just past
typical publish latency) first; falls back to `now-10h` (one cycle
earlier) if the fresher cycle isn't yet on the datamart.
Public so tests can exercise selection logic without going through Oban.
"""
@spec pick_run_time(DateTime.t()) :: DateTime.t()
def pick_run_time(%DateTime{} = now) do
fresh = now |> DateTime.add(-4, :hour) |> HrdpsClient.nearest_hrdps_cycle() |> DateTime.truncate(:second)
if HrdpsClient.cycle_available?(fresh) do
fresh
else
now |> DateTime.add(-10, :hour) |> HrdpsClient.nearest_hrdps_cycle() |> DateTime.truncate(:second)
end
end
end