defmodule Microwaveprop.Weather.MrmsCache do @moduledoc """ Node-local ETS cache of the latest MRMS PrecipRate grid, regridded onto the 0.125° propagation grid. Mirrors the pattern used by `Microwaveprop.Propagation.ScoreCache` and `Microwaveprop.Weather.GridCache` — a single GenServer owns the table, callers read directly from ETS, and `broadcast_put/2` fans updates out to peer nodes via PubSub. Only one node runs `Microwaveprop.Workers.MrmsFetchWorker` per cron tick (Oban Pro Smart engine picks a leader), so the other nodes get the grid via the `"mrms:cache"` topic and stay in sync without each one paying the 1 MB fetch + wgrib2 regrid cost. """ use GenServer alias Phoenix.PubSub @table :mrms_cache @topic "mrms:cache" @pubsub Microwaveprop.PubSub @type rain_grid :: %{{float(), float()} => float()} @spec start_link(keyword()) :: GenServer.on_start() def start_link(opts), do: GenServer.start_link(__MODULE__, opts, name: __MODULE__) @doc "Return the cached `{valid_time, rain_grid}` or `:miss`." @spec fetch() :: {:ok, DateTime.t(), rain_grid()} | :miss def fetch do case :ets.lookup(@table, :current) do [{_, valid_time, grid}] -> {:ok, valid_time, grid} [] -> :miss end end @doc "Current MRMS valid_time if anything is cached." @spec valid_time() :: DateTime.t() | nil def valid_time do case fetch() do {:ok, vt, _} -> vt :miss -> nil end end @spec put(DateTime.t(), rain_grid()) :: :ok def put(valid_time, grid) do :ets.insert(@table, {:current, valid_time, grid}) :ok end @doc "Insert locally and broadcast to peer nodes." @spec broadcast_put(DateTime.t(), rain_grid()) :: :ok def broadcast_put(valid_time, grid) do put(valid_time, grid) PubSub.broadcast(@pubsub, @topic, {:mrms_cache_refresh, valid_time, grid}) :ok end @spec clear() :: :ok def clear do :ets.delete_all_objects(@table) :ok end @impl true def init(_opts) do :ets.new(@table, [:set, :named_table, :public, read_concurrency: true]) PubSub.subscribe(@pubsub, @topic) {:ok, %{}} end @impl true def handle_info({:mrms_cache_refresh, valid_time, grid}, state) do put(valid_time, grid) {:noreply, state} end def handle_info(_msg, state), do: {:noreply, state} end