ContactWeatherEnqueueWorker: dispatch NarrFetchWorker for historical backfill

Swap the body of era5_jobs_for_contact/1 to enqueue NarrFetchWorker
instead of Era5FetchWorker, and snap qso_timestamp via
NarrClient.snap_to_analysis_hour/1 so it lands on a 3-hourly NARR
analysis slot (00/03/06/09/12/15/18/21 UTC) — NarrFetchWorker raises
on anything else.

The :era5 type key, the era5_jobs_for_contact function name, and
the era5_status field are kept as historical artifacts — they
still refer to the era5_profiles table, which is reused 1:1 by
NARR. A schema rename is a follow-up PR.

The CDS Era5* workers (Era5FetchWorker, Era5SubmitWorker,
Era5PollWorker, Era5MonthBatchWorker, Era5BatchClient) are now
unreachable from the live submission flow. They stay in tree until
the deletion follow-up PR.
This commit is contained in:
Graham McIntire 2026-04-15 18:49:02 -05:00
parent e44d7043e5
commit 4aa23ae67f
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
2 changed files with 111 additions and 3 deletions

View file

@ -5,9 +5,10 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorker do
alias Microwaveprop.Radio alias Microwaveprop.Radio
alias Microwaveprop.Weather alias Microwaveprop.Weather
alias Microwaveprop.Weather.HrrrClient alias Microwaveprop.Weather.HrrrClient
alias Microwaveprop.Workers.Era5FetchWorker alias Microwaveprop.Weather.NarrClient
alias Microwaveprop.Workers.HrrrFetchWorker alias Microwaveprop.Workers.HrrrFetchWorker
alias Microwaveprop.Workers.IemreFetchWorker alias Microwaveprop.Workers.IemreFetchWorker
alias Microwaveprop.Workers.NarrFetchWorker
alias Microwaveprop.Workers.TerrainProfileWorker alias Microwaveprop.Workers.TerrainProfileWorker
alias Microwaveprop.Workers.WeatherFetchWorker alias Microwaveprop.Workers.WeatherFetchWorker
@ -178,10 +179,15 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorker do
|> Enum.uniq_by(fn changeset -> changeset.changes.args end) |> Enum.uniq_by(fn changeset -> changeset.changes.args end)
end end
# The function name is a historical artifact: the `:era5` type key still
# refers to the `era5_profiles` table (reused 1:1), but this now dispatches
# `NarrFetchWorker` against NCEP NARR — the CDS/ERA5 pipeline is gone.
defp era5_jobs_for_contact(%{pos1: nil}), do: [] defp era5_jobs_for_contact(%{pos1: nil}), do: []
defp era5_jobs_for_contact(contact) do defp era5_jobs_for_contact(contact) do
valid_time = %{DateTime.truncate(contact.qso_timestamp, :second) | minute: 0, second: 0} # NARR analyses are 3-hourly (00/03/06/09/12/15/18/21 UTC). NarrFetchWorker
# rejects anything else, so snap here in the caller.
valid_time = NarrClient.snap_to_analysis_hour(contact.qso_timestamp)
contact contact
|> Radio.contact_path_points() |> Radio.contact_path_points()
@ -193,7 +199,7 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorker do
[] []
else else
[ [
Era5FetchWorker.new(%{ NarrFetchWorker.new(%{
"lat" => rlat, "lat" => rlat,
"lon" => rlon, "lon" => rlon,
"valid_time" => DateTime.to_iso8601(valid_time) "valid_time" => DateTime.to_iso8601(valid_time)

View file

@ -4,6 +4,7 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorkerTest do
alias Microwaveprop.Radio.Contact alias Microwaveprop.Radio.Contact
alias Microwaveprop.Terrain.ElevationClient alias Microwaveprop.Terrain.ElevationClient
alias Microwaveprop.Weather alias Microwaveprop.Weather
alias Microwaveprop.Weather.Era5Profile
alias Microwaveprop.Weather.HrrrClient alias Microwaveprop.Weather.HrrrClient
alias Microwaveprop.Weather.IemClient alias Microwaveprop.Weather.IemClient
alias Microwaveprop.Workers.ContactWeatherEnqueueWorker alias Microwaveprop.Workers.ContactWeatherEnqueueWorker
@ -561,6 +562,107 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorkerTest do
end end
end end
describe "build_era5_jobs/1" do
test "builds NarrFetchWorker jobs for each rounded path point" do
contact =
create_contact(%{
qso_timestamp: ~U[2010-06-15 13:42:00Z],
pos1: %{"lat" => 32.907, "lon" => -97.038},
pos2: nil
})
jobs = ContactWeatherEnqueueWorker.build_era5_jobs([contact])
assert length(jobs) == 1
job = hd(jobs)
assert job.changes.worker == "Microwaveprop.Workers.NarrFetchWorker"
# 32.907 → 33.0 and -97.038 → -97.0 after 0.25° snap
assert job.changes.args["lat"] == 33.0
assert job.changes.args["lon"] == -97.0
end
test "snaps valid_time to the nearest 3-hourly NARR analysis slot" do
contact =
create_contact(%{
qso_timestamp: ~U[2010-06-15 13:42:00Z],
pos1: %{"lat" => 32.907, "lon" => -97.038},
pos2: nil
})
jobs = ContactWeatherEnqueueWorker.build_era5_jobs([contact])
job = hd(jobs)
# 13:42 UTC snaps down to the 12:00 UTC NARR analysis slot.
assert job.changes.args["valid_time"] == "2010-06-15T12:00:00Z"
end
test "builds one job per distinct rounded path point" do
contact =
create_contact(%{
qso_timestamp: ~U[2010-06-15 12:00:00Z],
pos1: %{"lat" => 32.907, "lon" => -97.038},
pos2: %{"lat" => 30.3, "lon" => -97.7}
})
jobs = ContactWeatherEnqueueWorker.build_era5_jobs([contact])
# pos1, midpoint, pos2 round to three distinct 0.25° grid cells.
assert length(jobs) == 3
assert Enum.all?(jobs, &(&1.changes.worker == "Microwaveprop.Workers.NarrFetchWorker"))
end
test "skips path points that already have an era5_profile row" do
contact =
create_contact(%{
qso_timestamp: ~U[2010-06-15 13:42:00Z],
pos1: %{"lat" => 32.907, "lon" => -97.038},
pos2: nil
})
# pos1 (32.907, -97.038) snaps to the (33.0, -97.0) 0.25° grid cell
%Era5Profile{}
|> Era5Profile.changeset(%{
lat: 33.0,
lon: -97.0,
valid_time: ~U[2010-06-15 12:00:00Z],
profile: []
})
|> Repo.insert!()
jobs = ContactWeatherEnqueueWorker.build_era5_jobs([contact])
assert jobs == []
end
test "skips QSOs without pos1" do
contact = create_contact(%{pos1: nil})
assert ContactWeatherEnqueueWorker.build_era5_jobs([contact]) == []
end
test "deduplicates jobs with identical args across contacts" do
q1 =
create_contact(%{
station1: "A1",
qso_timestamp: ~U[2010-06-15 12:10:00Z],
pos1: %{"lat" => 32.907, "lon" => -97.038},
pos2: nil
})
q2 =
create_contact(%{
station1: "A2",
qso_timestamp: ~U[2010-06-15 13:42:00Z],
pos1: %{"lat" => 32.907, "lon" => -97.038},
pos2: nil
})
jobs = ContactWeatherEnqueueWorker.build_era5_jobs([q1, q2])
# Both snap to 12:00Z and round to the same 0.25° grid cell → 1 job.
assert length(jobs) == 1
assert hd(jobs).changes.worker == "Microwaveprop.Workers.NarrFetchWorker"
end
end
describe "enqueue_for_contact/1" do describe "enqueue_for_contact/1" do
setup do setup do
Req.Test.stub(IemClient, fn conn -> Req.Test.stub(IemClient, fn conn ->