prop/lib/microwaveprop/workers/weather_fetch_worker.ex
Graham McIntire 4468fb3b61
Increase Oban retry attempts and backoff for external API workers
20 attempts with exponential backoff capped at 6 hours ensures
data is eventually fetched even during extended rate limiting or
API outages (~3 days of retrying before giving up).
2026-03-29 17:03:56 -05:00

110 lines
3.4 KiB
Elixir

defmodule Microwaveprop.Workers.WeatherFetchWorker do
@moduledoc false
use Oban.Worker, queue: :weather, max_attempts: 20
alias Microwaveprop.Repo
alias Microwaveprop.Weather
alias Microwaveprop.Weather.IemClient
alias Microwaveprop.Weather.SoundingParams
alias Microwaveprop.Weather.Station
@impl Oban.Worker
def backoff(%Oban.Job{attempt: attempt}) do
min(120 * Integer.pow(2, attempt - 1), _six_hours = 21_600)
end
@impl Oban.Worker
def perform(%Oban.Job{args: %{"fetch_type" => "asos"} = args}) do
%{
"station_id" => station_id,
"station_code" => station_code,
"start_dt" => start_dt_str,
"end_dt" => end_dt_str
} = args
{:ok, start_dt, _} = DateTime.from_iso8601(start_dt_str)
{:ok, end_dt, _} = DateTime.from_iso8601(end_dt_str)
case Repo.get(Station, station_id) do
nil ->
:ok
station ->
if Weather.has_surface_observations?(station.id, start_dt, end_dt) do
:ok
else
case IemClient.fetch_asos(station_code, start_dt, end_dt) do
{:ok, rows} ->
Enum.each(rows, fn row ->
if row.observed_at, do: Weather.upsert_surface_observation(station, row)
end)
:ok
{:error, reason} ->
{:error, reason}
end
end
end
end
def perform(%Oban.Job{args: %{"fetch_type" => "raob"} = args}) do
%{
"station_id" => station_id,
"station_code" => station_code,
"sounding_time" => sounding_time_str
} = args
{:ok, sounding_time, _} = DateTime.from_iso8601(sounding_time_str)
case Repo.get(Station, station_id) do
nil ->
:ok
station ->
if Weather.has_sounding?(station.id, sounding_time) do
:ok
else
case IemClient.fetch_raob(station_code, sounding_time) do
{:ok, [parsed | _]} ->
params = SoundingParams.derive(parsed.profile)
sounding_attrs =
if params do
%{
observed_at: parsed.observed_at,
profile: parsed.profile,
level_count: params.level_count,
surface_pressure_mb: params.surface_pressure_mb,
surface_temp_c: params.surface_temp_c,
surface_dewpoint_c: params.surface_dewpoint_c,
surface_refractivity: params.surface_refractivity,
min_refractivity_gradient: params.min_refractivity_gradient,
boundary_layer_depth_m: params.boundary_layer_depth_m,
precipitable_water_mm: params.precipitable_water_mm,
k_index: params.k_index,
lifted_index: params.lifted_index,
ducting_detected: params.ducting_detected,
duct_characteristics: params.duct_characteristics
}
else
%{
observed_at: parsed.observed_at,
profile: parsed.profile,
level_count: length(parsed.profile)
}
end
Weather.upsert_sounding(station, sounding_attrs)
:ok
{:ok, []} ->
:ok
{:error, reason} ->
{:error, reason}
end
end
end
end
end