2,019 contacts sat permanently in weather_status=:queued even after their ASOS data had been ingested. The only code path that flipped :queued → :complete was MicrowavepropWeb.ContactLive.Show on page view — contacts that nobody visited stayed :queued forever. Adds Weather.reconcile_weather_statuses/0, a single SQL UPDATE that flips every :queued contact whose ±2h / 150km window now contains at least one surface observation. Called at the tail of the existing ContactWeatherEnqueueWorker cron so the correction runs regardless of whether anyone opens the UI. Visible effect: the status page's "Weather done" count (previously capped at 79,975 while data kept landing) will converge on the real total after the next cron tick. 2838 tests + credo green.
562 lines
19 KiB
Elixir
562 lines
19 KiB
Elixir
defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorker do
|
||
@moduledoc false
|
||
use Oban.Worker, queue: :enqueue, max_attempts: 3
|
||
|
||
alias Microwaveprop.Propagation.Grid
|
||
alias Microwaveprop.Radio
|
||
alias Microwaveprop.Radio.Contact
|
||
alias Microwaveprop.Weather
|
||
alias Microwaveprop.Weather.HrrrPointEnqueuer
|
||
alias Microwaveprop.Weather.NarrClient
|
||
alias Microwaveprop.Weather.NexradClient
|
||
alias Microwaveprop.Workers.IemreFetchWorker
|
||
alias Microwaveprop.Workers.MechanismClassifyWorker
|
||
alias Microwaveprop.Workers.NarrFetchWorker
|
||
alias Microwaveprop.Workers.RadarFrameWorker
|
||
alias Microwaveprop.Workers.TerrainProfileWorker
|
||
alias Microwaveprop.Workers.WeatherFetchWorker
|
||
|
||
@asos_radius_km 150
|
||
@sounding_radius_km 300
|
||
# Oban jobs have ~149 params each; PG limit is 65535 params per query
|
||
@insert_batch_size 400
|
||
|
||
@doc """
|
||
Enqueue all enrichment jobs (weather, HRRR, terrain, IEMRE) for a single contact.
|
||
Called directly from submission flow — no Oban indirection.
|
||
Optionally pass a list of types to limit which enrichments run.
|
||
"""
|
||
@spec enqueue_for_contact(Contact.t(), [atom()]) :: :ok
|
||
def enqueue_for_contact(contact, types \\ [:hrrr, :weather, :terrain, :iemre, :radar, :mechanism]) do
|
||
contact = Radio.ensure_positions!(contact)
|
||
# BackfillEnqueueWorker selects any contact with at least one
|
||
# *_status in [:pending, :queued, :failed] and calls this with all
|
||
# types. Filter out types that are already :complete for this
|
||
# contact so mark_status!/3 doesn't demote them back to :queued on
|
||
# every cron tick — that caused tens of thousands of
|
||
# mechanism/terrain rows to ping-pong between :complete and :queued.
|
||
types = Enum.reject(types, &already_complete?(contact, &1))
|
||
jobs_by_type = build_jobs_by_type(contact, types)
|
||
|
||
bulk_jobs =
|
||
jobs_by_type[:weather] ++
|
||
jobs_by_type[:terrain] ++
|
||
jobs_by_type[:iemre] ++
|
||
jobs_by_type[:radar] ++
|
||
jobs_by_type[:mechanism]
|
||
|
||
if bulk_jobs != [] do
|
||
insert_all_chunked(bulk_jobs)
|
||
end
|
||
|
||
# HRRR enrichment runs via the Rust hrrr-point-worker via the
|
||
# hrrr_fetch_tasks table (Phase 3 Stream C). Grouping by valid_time
|
||
# and UPSERT-union means a backfill tick that adds a new contact in
|
||
# an already-scheduled hour collapses into the existing row instead
|
||
# of creating a duplicate fetch.
|
||
_ =
|
||
if :hrrr in types do
|
||
{:ok, _} = HrrrPointEnqueuer.enqueue_for_contacts([contact])
|
||
end
|
||
|
||
# NARR jobs must go through Oban.insert/1 so the worker's unique
|
||
# constraint is honored; Oban OSS insert_all does not check uniqueness.
|
||
insert_unique(jobs_by_type[:narr])
|
||
|
||
_ = mark_enrichment_statuses(contact, types, jobs_by_type)
|
||
|
||
:ok
|
||
end
|
||
|
||
defp build_jobs_by_type(contact, types) do
|
||
%{
|
||
weather: if(:weather in types, do: build_weather_jobs([contact]), else: []),
|
||
# hrrr jobs are routed to hrrr_fetch_tasks (not Oban) but the
|
||
# status-marking path still wants to know whether any HRRR work
|
||
# was created for this contact, so we mirror the list-shape.
|
||
hrrr: if(:hrrr in types, do: hrrr_placeholder_jobs([contact]), else: []),
|
||
terrain: if(:terrain in types, do: build_terrain_jobs([contact]), else: []),
|
||
iemre: if(:iemre in types, do: build_iemre_jobs([contact]), else: []),
|
||
narr: if(:narr in types, do: build_narr_jobs([contact]), else: []),
|
||
radar: if(:radar in types, do: build_radar_jobs([contact]), else: []),
|
||
mechanism: if(:mechanism in types, do: build_mechanism_jobs([contact]), else: [])
|
||
}
|
||
end
|
||
|
||
# Placeholder for the HRRR job-list signal. Returns a truthy singleton
|
||
# if the contact has at least one HRRR-eligible path point so
|
||
# mark_hrrr_status!/3 flips hrrr_queued on the contact row. Actual
|
||
# scheduling happens via HrrrPointEnqueuer.enqueue_for_contacts/1
|
||
# above. Returns [] for pre-coverage contacts (NARR's territory) so
|
||
# the status stays :unavailable, and [] for contacts whose HRRR
|
||
# profiles are already fully present in the DB so mark_hrrr_status!/3
|
||
# flips them directly to :complete — otherwise backfill re-flags the
|
||
# contact as :queued every cron cycle forever (HrrrPointEnqueuer
|
||
# sees 0 missing points, inserts nothing, but the status stays
|
||
# :queued and the contact is picked up again next tick).
|
||
defp hrrr_placeholder_jobs(contacts) do
|
||
Enum.flat_map(contacts, fn contact ->
|
||
cond do
|
||
is_nil(contact.pos1) -> []
|
||
not Grid.contains?(contact.pos1) -> []
|
||
NarrClient.in_coverage?(contact.qso_timestamp) -> []
|
||
Weather.hrrr_data_fully_present?(contact) -> []
|
||
true -> [contact.id]
|
||
end
|
||
end)
|
||
end
|
||
|
||
defp mark_enrichment_statuses(contact, types, jobs_by_type) do
|
||
ids = [contact.id]
|
||
|
||
_ =
|
||
if contact.pos1 do
|
||
mark_pos1_statuses(contact, ids, types, jobs_by_type)
|
||
end
|
||
|
||
if contact.pos1 && contact.pos2 do
|
||
mark_path_statuses(contact, ids, types, jobs_by_type)
|
||
else
|
||
# Path enrichments (terrain/radar/mechanism) need both endpoints.
|
||
# Marking :unavailable stops BackfillEnqueueWorker from re-scanning
|
||
# these contacts every cron cycle. Radio.reset_enrichment_statuses/2
|
||
# will flip them back to :pending if pos2 is added later.
|
||
mark_missing_path_statuses(ids, types)
|
||
end
|
||
end
|
||
|
||
defp mark_missing_path_statuses(ids, types) do
|
||
_ = if :terrain in types, do: Radio.set_enrichment_status!(ids, :terrain_status, :unavailable)
|
||
_ = if :radar in types, do: Radio.set_enrichment_status!(ids, :radar_status, :unavailable)
|
||
|
||
if :mechanism in types,
|
||
do: Radio.set_enrichment_status!(ids, :mechanism_status, :unavailable)
|
||
end
|
||
|
||
defp mark_pos1_statuses(contact, ids, types, jobs_by_type) do
|
||
_ = if :weather in types, do: mark_status!(ids, :weather_status, jobs_by_type[:weather])
|
||
_ = if :hrrr in types, do: mark_hrrr_status!(contact, ids, jobs_by_type[:hrrr])
|
||
if :iemre in types, do: mark_status!(ids, :iemre_status, jobs_by_type[:iemre])
|
||
end
|
||
|
||
defp mark_path_statuses(contact, ids, types, jobs_by_type) do
|
||
_ = if :terrain in types, do: mark_status!(ids, :terrain_status, jobs_by_type[:terrain])
|
||
_ = if :radar in types, do: mark_radar_status!(contact, ids, jobs_by_type[:radar])
|
||
|
||
if :mechanism in types,
|
||
do: mark_status!(ids, :mechanism_status, jobs_by_type[:mechanism])
|
||
end
|
||
|
||
# IEM n0q composite reflectivity archive only has meaningful CONUS
|
||
# coverage post-2014 (pre that, the archive is spotty mosaics). For
|
||
# pre-2014 contacts we can't get radar, so pin to :unavailable rather
|
||
# than leaving it :pending forever.
|
||
defp mark_radar_status!(contact, ids, []) do
|
||
if NarrClient.in_coverage?(contact.qso_timestamp) do
|
||
Radio.set_enrichment_status!(ids, :radar_status, :unavailable)
|
||
else
|
||
Radio.set_enrichment_status!(ids, :radar_status, :queued)
|
||
end
|
||
end
|
||
|
||
defp mark_radar_status!(_contact, ids, [_ | _]), do: Radio.set_enrichment_status!(ids, :radar_status, :queued)
|
||
|
||
# Empty-job-list HRRR path has three terminal states:
|
||
#
|
||
# 1. pos1 outside the CONUS grid → :unavailable. hrrr_point_rs silently
|
||
# returns zero profiles for OCONUS points and the backfill cron
|
||
# would otherwise re-flag :queued every tick forever.
|
||
# 2. pre-2014 contacts (NarrClient.in_coverage?) → :unavailable so the
|
||
# :narr filter in BackfillEnqueueWorker picks them up.
|
||
# 3. modern CONUS contact whose profiles already exist → :complete.
|
||
defp mark_hrrr_status!(contact, ids, []) do
|
||
cond do
|
||
not Grid.contains?(contact.pos1) ->
|
||
Radio.set_enrichment_status!(ids, :hrrr_status, :unavailable)
|
||
|
||
NarrClient.in_coverage?(contact.qso_timestamp) ->
|
||
Radio.set_enrichment_status!(ids, :hrrr_status, :unavailable)
|
||
|
||
true ->
|
||
Radio.set_enrichment_status!(ids, :hrrr_status, :complete)
|
||
end
|
||
end
|
||
|
||
defp mark_hrrr_status!(_contact, ids, [_ | _]), do: Radio.set_enrichment_status!(ids, :hrrr_status, :queued)
|
||
|
||
@impl Oban.Worker
|
||
def perform(%Oban.Job{}) do
|
||
enqueue_weather_jobs()
|
||
enqueue_hrrr_jobs()
|
||
enqueue_terrain_jobs()
|
||
enqueue_iemre_jobs()
|
||
|
||
# After enqueuing, sweep any :queued contacts whose data has
|
||
# already landed (from a prior run's ingestion) but whose
|
||
# weather_status was never flipped. Without this, contacts stayed
|
||
# visible to the backfill scan forever.
|
||
case Weather.reconcile_weather_statuses() do
|
||
{:ok, n} when n > 0 ->
|
||
require Logger
|
||
|
||
Logger.info("ContactWeatherEnqueueWorker: reconciled #{n} weather_status → :complete")
|
||
|
||
_ ->
|
||
:ok
|
||
end
|
||
|
||
:ok
|
||
end
|
||
|
||
defp enqueue_weather_jobs do
|
||
case Radio.unprocessed_contacts() do
|
||
[] ->
|
||
:ok
|
||
|
||
contacts ->
|
||
_ = Radio.backfill_distances(contacts)
|
||
|
||
jobs = build_weather_jobs(contacts)
|
||
|
||
if jobs != [] do
|
||
insert_all_chunked(jobs)
|
||
end
|
||
|
||
ids = Enum.map(contacts, & &1.id)
|
||
_ = Radio.mark_weather_queued!(ids)
|
||
|
||
enqueue_weather_jobs()
|
||
end
|
||
end
|
||
|
||
defp enqueue_hrrr_jobs do
|
||
case Radio.unprocessed_hrrr_contacts() do
|
||
[] ->
|
||
:ok
|
||
|
||
contacts ->
|
||
# Rust Stream C path: route into hrrr_fetch_tasks instead of
|
||
# Oban. The enqueuer groups by valid_time and UPSERTs so a
|
||
# backfill cron sweep that rediscovers the same contacts
|
||
# simply refreshes the existing hourly row.
|
||
{:ok, _} = HrrrPointEnqueuer.enqueue_for_contacts(contacts)
|
||
|
||
ids = Enum.map(contacts, & &1.id)
|
||
_ = Radio.mark_hrrr_queued!(ids)
|
||
|
||
enqueue_hrrr_jobs()
|
||
end
|
||
end
|
||
|
||
defp enqueue_terrain_jobs do
|
||
case Radio.unprocessed_terrain_contacts() do
|
||
[] ->
|
||
:ok
|
||
|
||
contacts ->
|
||
jobs = build_terrain_jobs(contacts)
|
||
|
||
if jobs != [] do
|
||
insert_all_chunked(jobs)
|
||
end
|
||
|
||
ids = Enum.map(contacts, & &1.id)
|
||
_ = Radio.mark_terrain_queued!(ids)
|
||
|
||
enqueue_terrain_jobs()
|
||
end
|
||
end
|
||
|
||
@spec build_terrain_jobs([Contact.t()]) :: [Ecto.Changeset.t()]
|
||
def build_terrain_jobs(contacts) do
|
||
contacts
|
||
|> Enum.filter(fn contact -> contact.pos1 && contact.pos2 end)
|
||
|> Enum.map(fn contact ->
|
||
TerrainProfileWorker.new(%{"contact_id" => contact.id})
|
||
end)
|
||
|> Enum.uniq_by(fn changeset -> changeset.changes.args end)
|
||
end
|
||
|
||
@spec build_radar_jobs([Contact.t()]) :: [Ecto.Changeset.t()]
|
||
def build_radar_jobs(contacts) do
|
||
# Group contacts by their 5-min NEXRAD frame so the worker can
|
||
# fetch+decode the ~5 MB PNG once per frame and iterate every
|
||
# contact in that frame against the in-memory pixel buffer.
|
||
# Historical backlog averages ~10 contacts per distinct frame,
|
||
# collapsing the per-job cost by roughly that factor.
|
||
contacts
|
||
|> Enum.filter(fn contact ->
|
||
contact.pos1 && contact.pos2 && not NarrClient.in_coverage?(contact.qso_timestamp)
|
||
end)
|
||
|> Enum.group_by(fn contact ->
|
||
contact.qso_timestamp
|
||
|> DateTime.from_naive!("Etc/UTC")
|
||
|> NexradClient.round_to_5min()
|
||
end)
|
||
|> Enum.map(fn {frame_ts, batch} ->
|
||
RadarFrameWorker.new(%{
|
||
"frame_ts" => DateTime.to_iso8601(frame_ts),
|
||
"contact_ids" => Enum.map(batch, & &1.id)
|
||
})
|
||
end)
|
||
end
|
||
|
||
@spec build_mechanism_jobs([Contact.t()]) :: [Ecto.Changeset.t()]
|
||
def build_mechanism_jobs(contacts) do
|
||
contacts
|
||
|> Enum.filter(fn contact -> contact.pos1 && contact.pos2 end)
|
||
|> Enum.map(fn contact ->
|
||
MechanismClassifyWorker.new(%{"contact_id" => contact.id})
|
||
end)
|
||
|> Enum.uniq_by(fn changeset -> changeset.changes.args end)
|
||
end
|
||
|
||
@spec build_weather_jobs([Contact.t()]) :: [Ecto.Changeset.t()]
|
||
def build_weather_jobs(contacts) do
|
||
contacts
|
||
|> Enum.flat_map(&jobs_for_contact/1)
|
||
|> Enum.uniq_by(fn changeset -> changeset.changes.args end)
|
||
end
|
||
|
||
@spec build_iemre_jobs([Contact.t()]) :: [Ecto.Changeset.t()]
|
||
def build_iemre_jobs(contacts) do
|
||
contacts
|
||
|> Enum.flat_map(&iemre_job_for_contact/1)
|
||
|> Enum.uniq_by(fn changeset -> changeset.changes.args end)
|
||
end
|
||
|
||
@spec build_narr_jobs([Contact.t()]) :: [Ecto.Changeset.t()]
|
||
def build_narr_jobs(contacts) do
|
||
contacts
|
||
|> Enum.flat_map(&narr_jobs_for_contact/1)
|
||
|> Enum.uniq_by(fn changeset -> changeset.changes.args end)
|
||
end
|
||
|
||
defp narr_jobs_for_contact(%{pos1: nil}), do: []
|
||
|
||
defp narr_jobs_for_contact(contact) do
|
||
# 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)
|
||
|
||
# NCEI's NARR archive covers 1979-01-01 → 2014-10-02. Post-coverage contacts
|
||
# (hrrr_status = :unavailable but qso_timestamp >= 2014-10-02) are HRRR's
|
||
# responsibility — NARR would just 404.
|
||
if NarrClient.in_coverage?(valid_time) do
|
||
narr_jobs_for_path(contact, valid_time)
|
||
else
|
||
[]
|
||
end
|
||
end
|
||
|
||
defp narr_jobs_for_path(contact, valid_time) do
|
||
contact
|
||
|> Radio.contact_path_points()
|
||
|> Enum.flat_map(fn {lat, lon} ->
|
||
rlat = Float.round(lat * 4) / 4
|
||
rlon = Float.round(lon * 4) / 4
|
||
|
||
if Weather.find_nearest_narr(rlat, rlon, valid_time) do
|
||
[]
|
||
else
|
||
[
|
||
NarrFetchWorker.new(%{
|
||
"lat" => rlat,
|
||
"lon" => rlon,
|
||
"valid_time" => DateTime.to_iso8601(valid_time)
|
||
})
|
||
]
|
||
end
|
||
end)
|
||
end
|
||
|
||
defp jobs_for_contact(contact) do
|
||
contact
|
||
|> Radio.contact_path_points()
|
||
|> Enum.flat_map(fn {lat, lon} ->
|
||
build_asos_jobs(lat, lon, contact.qso_timestamp) ++
|
||
build_raob_jobs(lat, lon, contact.qso_timestamp)
|
||
end)
|
||
end
|
||
|
||
defp enqueue_iemre_jobs do
|
||
case Radio.unprocessed_iemre_contacts() do
|
||
[] ->
|
||
:ok
|
||
|
||
contacts ->
|
||
jobs = build_iemre_jobs(contacts)
|
||
|
||
if jobs != [] do
|
||
insert_all_chunked(jobs)
|
||
end
|
||
|
||
ids = Enum.map(contacts, & &1.id)
|
||
_ = Radio.mark_iemre_queued!(ids)
|
||
|
||
enqueue_iemre_jobs()
|
||
end
|
||
end
|
||
|
||
defp build_asos_jobs(lat, lon, timestamp) do
|
||
start_dt = DateTime.add(timestamp, -2 * 3600, :second)
|
||
end_dt = DateTime.add(timestamp, 2 * 3600, :second)
|
||
|
||
stations = Weather.nearby_stations(lat, lon, "asos", @asos_radius_km)
|
||
|
||
# A ±2h window around the QSO can straddle UTC midnight, so enumerate
|
||
# every date the window touches. Each (station, date) becomes one
|
||
# asos_day job — Oban's unique-args dedup collapses cross-QSO
|
||
# repeats (contact A's KDFW/2021-08-21 and contact B's KDFW/2021-08-21
|
||
# share args → one job). A full-day fetch returns ~24 hourly rows,
|
||
# amortizing the IemRateLimiter gap over 24× the previous payload.
|
||
dates = window_dates(start_dt, end_dt)
|
||
|
||
pairs =
|
||
for station <- stations, date <- dates do
|
||
{station, date}
|
||
end
|
||
|
||
covered =
|
||
pairs
|
||
|> Enum.map(fn {s, d} -> {s.id, d} end)
|
||
|> Weather.station_day_pairs_covered()
|
||
|
||
pairs
|
||
|> Enum.reject(fn {s, d} -> MapSet.member?(covered, {s.id, d}) end)
|
||
|> Enum.map(fn {station, date} ->
|
||
WeatherFetchWorker.new(%{
|
||
"fetch_type" => "asos_day",
|
||
"station_id" => station.id,
|
||
"station_code" => station.station_code,
|
||
"date" => Date.to_iso8601(date)
|
||
})
|
||
end)
|
||
end
|
||
|
||
defp window_dates(start_dt, end_dt) do
|
||
start_date = DateTime.to_date(start_dt)
|
||
end_date = DateTime.to_date(end_dt)
|
||
|
||
if Date.compare(start_date, end_date) == :eq do
|
||
[start_date]
|
||
else
|
||
[start_date, end_date]
|
||
end
|
||
end
|
||
|
||
defp iemre_job_for_contact(%{pos1: nil}), do: []
|
||
|
||
defp iemre_job_for_contact(contact) do
|
||
date = DateTime.to_date(contact.qso_timestamp)
|
||
|
||
contact
|
||
|> Radio.contact_path_points()
|
||
|> Enum.flat_map(fn {lat, lon} ->
|
||
{rlat, rlon} = Weather.round_to_iemre_grid(lat, lon)
|
||
|
||
if Weather.has_iemre_observation?(rlat, rlon, date) do
|
||
[]
|
||
else
|
||
[
|
||
IemreFetchWorker.new(%{
|
||
"lat" => rlat,
|
||
"lon" => rlon,
|
||
"date" => Date.to_iso8601(date)
|
||
})
|
||
]
|
||
end
|
||
end)
|
||
end
|
||
|
||
defp build_raob_jobs(lat, lon, timestamp) do
|
||
build_raob_jobs(lat, lon, timestamp, @sounding_radius_km, nil)
|
||
end
|
||
|
||
defp build_raob_jobs(lat, lon, timestamp, radius_km, contact_id) do
|
||
sounding_times = Weather.sounding_times_around(timestamp)
|
||
stations = Weather.nearby_stations(lat, lon, "sounding", radius_km)
|
||
station_ids = Enum.map(stations, & &1.id)
|
||
covered = Weather.station_ids_with_soundings(station_ids, sounding_times)
|
||
|
||
for station <- stations,
|
||
sounding_time <- sounding_times,
|
||
not MapSet.member?(covered, {station.id, sounding_time}) do
|
||
args =
|
||
Map.merge(
|
||
%{
|
||
"fetch_type" => "raob",
|
||
"station_id" => station.id,
|
||
"station_code" => station.station_code,
|
||
"sounding_time" => DateTime.to_iso8601(sounding_time)
|
||
},
|
||
if(contact_id, do: %{"contact_id" => contact_id}, else: %{})
|
||
)
|
||
|
||
WeatherFetchWorker.new(args)
|
||
end
|
||
end
|
||
|
||
@doc """
|
||
Enqueue RAOB fetch jobs for the given contact at a progressively wider
|
||
radius until the radius yields at least one sounding station. Passes
|
||
the contact id through so `WeatherFetchWorker` can PubSub the contact
|
||
detail page when each sounding lands.
|
||
|
||
Returns `{:ok, %{radius_km, jobs_enqueued}}`, or `{:error, :no_stations}`
|
||
when even 1000 km finds nothing.
|
||
"""
|
||
@spec enqueue_raob_fetch_with_widening(Contact.t()) ::
|
||
{:ok, %{radius_km: pos_integer(), jobs_enqueued: non_neg_integer()}}
|
||
| {:error, :no_stations}
|
||
| {:error, :no_coords}
|
||
def enqueue_raob_fetch_with_widening(contact) do
|
||
case Radio.contact_path_points(contact) do
|
||
[] ->
|
||
{:error, :no_coords}
|
||
|
||
[{lat, lon} | _] ->
|
||
do_widening_raob(lat, lon, contact)
|
||
end
|
||
end
|
||
|
||
defp do_widening_raob(lat, lon, contact) do
|
||
Enum.reduce_while([150, 300, 600, 1000], {:error, :no_stations}, fn radius, _acc ->
|
||
stations = Weather.nearby_stations(lat, lon, "sounding", radius)
|
||
|
||
if stations == [] do
|
||
{:cont, {:error, :no_stations}}
|
||
else
|
||
{:halt, enqueue_raob_at_radius(lat, lon, contact, radius)}
|
||
end
|
||
end)
|
||
end
|
||
|
||
defp enqueue_raob_at_radius(lat, lon, contact, radius) do
|
||
jobs = build_raob_jobs(lat, lon, contact.qso_timestamp, radius, contact.id)
|
||
if jobs != [], do: insert_all_chunked(jobs)
|
||
{:ok, %{radius_km: radius, jobs_enqueued: length(jobs)}}
|
||
end
|
||
|
||
defp insert_all_chunked(jobs) do
|
||
jobs
|
||
|> Enum.chunk_every(@insert_batch_size)
|
||
|> Enum.each(&Oban.insert_all/1)
|
||
end
|
||
|
||
defp insert_unique(jobs) do
|
||
Enum.each(jobs, &Oban.insert/1)
|
||
end
|
||
|
||
defp mark_status!(ids, field, [_ | _]), do: Radio.set_enrichment_status!(ids, field, :queued)
|
||
defp mark_status!(ids, field, []), do: Radio.set_enrichment_status!(ids, field, :complete)
|
||
|
||
# :narr is a virtual type synthesized from hrrr_status = :unavailable;
|
||
# it has no column of its own, so there's nothing to "already be
|
||
# complete."
|
||
defp already_complete?(_contact, :narr), do: false
|
||
|
||
defp already_complete?(contact, type) do
|
||
Map.get(contact, :"#{type}_status") == :complete
|
||
end
|
||
end
|