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

78 lines
2.7 KiB
Elixir

defmodule Microwaveprop.Workers.IonosphereFetchWorker do
@moduledoc """
Polls GIRO DIDBase for real-time ionosonde measurements (foF2, foE,
foEs, hmF2, MUFD) from a handful of CONUS stations. Drives the 144
MHz sporadic-E and HF MUF scoring inputs.
Runs on a ~10-minute cron — GIRO publishes each station at 7.5-minute
cadence so a 10-minute poll guarantees we pick up every new sample
within one cycle without hammering the endpoint.
Pulls a 2-hour window each poll to catch any late-arriving samples
and to be resilient to the occasional missed run. Upserts are
idempotent on (station_code, valid_time).
"""
use Oban.Pro.Worker,
queue: :ionosphere,
max_attempts: 3,
unique: [period: 300, states: :incomplete]
alias Microwaveprop.Ionosphere
alias Microwaveprop.Ionosphere.GiroClient
require Logger
# CONUS stations that have been publishing reliably. Others (Boulder,
# Wallops, Austin, Idaho, Point Arguello) are in the GIRO catalog but
# intermittent — add them here once they come back online.
@stations [
%{code: "MHJ45", name: "Millstone Hill MA", lat: 42.6, lon: -71.5},
%{code: "AL945", name: "Alpena MI", lat: 45.1, lon: -83.6}
]
@lookback_seconds 2 * 3600
@impl Oban.Pro.Worker
def process(%Oban.Job{}) do
now = DateTime.truncate(DateTime.utc_now(), :second)
from_dt = DateTime.add(now, -@lookback_seconds, :second)
Enum.each(@stations, fn station ->
fetch_and_upsert(station, from_dt, now)
end)
:ok
end
@doc false
@spec stations() :: [map()]
def stations, do: @stations
defp fetch_and_upsert(%{code: code, name: name}, from_dt, to_dt) do
case GiroClient.fetch(code, from_dt, to_dt) do
{:ok, observations} ->
{:ok, count} = Ionosphere.upsert_observations(code, observations)
Logger.info("IonosphereFetch: #{code} (#{name}) upserted #{count} observations")
{:error, reason} ->
Logger.warning("IonosphereFetch: #{code} (#{name}) failed: #{format_reason(reason)}")
end
end
# GIRO's TLS cert chains through a CA that isn't always in the pod's
# bundle, surfacing as a Req.TransportError with a multi-line erl_alert
# string. Inspecting the full struct dumps ~400 chars of stacktrace-y
# noise per station per 10-min cycle. Compress to something grep-able.
defp format_reason("GIRO request failed: " <> tail) do
cond do
String.contains?(tail, ":unknown_ca") -> "TLS unknown_ca (CA bundle missing)"
String.contains?(tail, ":timeout") -> "timeout"
String.contains?(tail, ":econnrefused") -> "connection refused"
true -> String.slice(tail, 0, 120)
end
end
defp format_reason(reason) when is_binary(reason), do: reason
defp format_reason(reason), do: inspect(reason, printable_limit: 200)
end