prop/lib/microwaveprop/workers/hrrr_fetch_worker.ex
Graham McIntire bb590a3b64
Invert HRRR failure logic: only retry transient network/server errors
Instead of listing every permanent failure string, only retry on known
transient errors (5xx, 429, network exceptions). Everything else —
GRIB decode errors, 404s, range request failures, index out of range —
is cancelled immediately.
2026-03-30 09:28:20 -05:00

93 lines
3.1 KiB
Elixir

defmodule Microwaveprop.Workers.HrrrFetchWorker do
@moduledoc false
use Oban.Worker, queue: :hrrr, max_attempts: 20
alias Microwaveprop.Weather
alias Microwaveprop.Weather.HrrrClient
alias Microwaveprop.Weather.SoundingParams
require Logger
@impl Oban.Worker
def backoff(%Oban.Job{attempt: attempt}) do
# Exponential backoff: 2m, 4m, 8m, 16m, 32m, 1h, 2h, 4h, 6h, 6h, ...
min(120 * Integer.pow(2, attempt - 1), _six_hours = 21_600)
end
@impl Oban.Worker
def perform(%Oban.Job{args: args}) do
%{"lat" => raw_lat, "lon" => raw_lon, "valid_time" => valid_time_str} = args
{:ok, valid_time, _} = DateTime.from_iso8601(valid_time_str)
{lat, lon} = Weather.round_to_hrrr_grid(raw_lat, raw_lon)
if Weather.has_hrrr_profile?(lat, lon, valid_time) do
Logger.info("HRRR profile already exists for #{lat},#{lon} @ #{valid_time_str}")
:ok
else
Logger.info("Fetching HRRR profile for #{lat},#{lon} @ #{valid_time_str}")
case HrrrClient.fetch_profile(lat, lon, valid_time) do
{:ok, data} ->
params = SoundingParams.derive(data.profile)
levels = length(data.profile)
attrs =
maybe_add_derived_params(
%{
valid_time: valid_time,
lat: lat,
lon: lon,
run_time: data.run_time,
profile: data.profile,
hpbl_m: data.hpbl_m,
pwat_mm: data.pwat_mm,
surface_temp_c: data.surface_temp_c,
surface_dewpoint_c: data.surface_dewpoint_c,
surface_pressure_mb: data.surface_pressure_mb
},
params
)
Weather.upsert_hrrr_profile(attrs)
Logger.info("HRRR profile saved for #{lat},#{lon} @ #{valid_time_str} (#{levels} levels, run #{data.run_time})")
:ok
{:error, reason} ->
if transient_failure?(reason) do
Logger.error("HRRR transient error for #{lat},#{lon} @ #{valid_time_str}: #{inspect(reason)}")
{:error, reason}
else
Logger.warning("HRRR permanent failure for #{lat},#{lon} @ #{valid_time_str}: #{inspect(reason)}")
{:cancel, reason}
end
end
end
end
# Only retry on transient network/server errors — everything else is permanent
defp transient_failure?(%{__exception__: true}), do: true
defp transient_failure?("HRRR idx HTTP " <> status), do: server_error?(status)
defp transient_failure?("HRRR grib HTTP " <> status), do: server_error?(status)
defp transient_failure?(_), do: false
defp server_error?(status) do
case Integer.parse(status) do
{code, _} when code in [429, 500, 502, 503, 504] -> true
_ -> false
end
end
defp maybe_add_derived_params(attrs, nil), do: attrs
defp maybe_add_derived_params(attrs, params) do
Map.merge(attrs, %{
surface_refractivity: params.surface_refractivity,
min_refractivity_gradient: params.min_refractivity_gradient,
ducting_detected: params.ducting_detected,
duct_characteristics: params.duct_characteristics
})
end
end