Enabled :error_handling, :unknown, :unmatched_returns, :extra_return, :missing_return in an earlier commit and landed a 129-warning baseline. Four parallel agents each fixed a directory slice: - Core contexts (29): Radio, Release, Weather, Beacons, Cache, Backtest.Features, Terrain.Srtm, Ionosphere.GiroClient, Propagation.RunTiming, Accounts.Scope, RepoListener. Fixes were (a) prefix side-effect calls (Task.start, Phoenix.PubSub, Logger, :ets.new) with _ = ; (b) tighten/widen specs that didn't match actual returns; (c) add missing @type t declarations; (d) drop dead parse_int(nil) clause. - Propagation + weather subdirs (15): FreshnessMonitor, NotifyListener, ScoreCache, ScoreCacheReconciler, Weather.FrontalAnalysis, Weather.Grib2.Extractor, Weather.Grib2.Wgrib2, GridCache, HrrrPointEnqueuer, NexradCache. Same patterns — mostly _ = on PubSub / :ets / Repo.insert_all; widened two specs (float -> number) where integer returns were reachable. - Workers (35): BackfillEnqueue, CanadianSoundingFetch, ContactImport, ContactWeatherEnqueue, GefsFetch, IemreFetch, NarrFetch, SolarIndex, TerrainProfile, WeatherFetch. Prefixed Repo.update_all / Radio.set_enrichment_status! / Weather.upsert_* side-effect calls. Fixed one :pattern_match in CanadianSoundingFetch.most_recent_sounding_time/1 where a tautological cond guard generated unreachable code. - Web + Mix tasks + lib_ml (46 of 50): controllers, LiveViews, UserAuth, and 11 mix tasks. Same prefix strategy. 4 remaining warnings originate in LiveTable.LiveResource dep macro expansion and can't be fixed without forking the dep — added .dialyzer_ignore.exs to suppress just those specific file:line pairs. Also wired ignore_warnings in mix.exs dialyzer config. mix dialyzer --format short | grep ^lib/ | wc -l -> 0 mix test: 2163 tests, 3 pre-existing flakes, 0 regressions.
79 lines
2.5 KiB
Elixir
79 lines
2.5 KiB
Elixir
defmodule Microwaveprop.Weather.NexradCache do
|
||
@moduledoc """
|
||
Node-local ETS cache of decoded NEXRAD n0q composite reflectivity frames.
|
||
Keyed by a 5-minute rounded timestamp. Stores the raw pixel buffer + image
|
||
width so per-point rain-cell extraction can skip the HTTP fetch + PNG decode
|
||
(which take 1-5 seconds for a ~5 MB CONUS-wide image).
|
||
|
||
The cache is populated by `Microwaveprop.Weather.NexradClient.fetch_rain_cells/4`
|
||
on its first call per 5-min window, then reused by every concurrent click
|
||
until the window rolls over.
|
||
"""
|
||
use GenServer
|
||
|
||
@table :nexrad_frame_cache
|
||
|
||
# Each frame is ~66 MB (12,200 × 5,400 palette-index bytes). 20 frames
|
||
# caps at ~1.3 GB — comfortable headroom under a 2 GB pod limit, and
|
||
# enough to hit on the common backfill pattern where a worker chews
|
||
# through contacts in one timestamp window before moving on. Adjust
|
||
# if pod memory limits change or the n0q geometry changes.
|
||
@max_entries 20
|
||
|
||
@type pixels :: binary()
|
||
@type width :: pos_integer()
|
||
|
||
@spec start_link(keyword()) :: GenServer.on_start()
|
||
def start_link(opts) do
|
||
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
||
end
|
||
|
||
@spec fetch(DateTime.t()) :: {:ok, pixels(), width()} | :miss
|
||
def fetch(rounded_ts) do
|
||
case :ets.lookup(@table, rounded_ts) do
|
||
[{_, pixels, width}] -> {:ok, pixels, width}
|
||
[] -> :miss
|
||
end
|
||
end
|
||
|
||
@spec put(DateTime.t(), pixels(), width()) :: :ok
|
||
def put(rounded_ts, pixels, width) do
|
||
:ets.insert(@table, {rounded_ts, pixels, width})
|
||
enforce_size_cap()
|
||
:ok
|
||
end
|
||
|
||
defp enforce_size_cap do
|
||
size = :ets.info(@table, :size)
|
||
|
||
if size > @max_entries do
|
||
# Drop the oldest-by-key entries until we're back under the cap.
|
||
# O(n log n) but n is ~20, so this runs in microseconds.
|
||
excess = size - @max_entries
|
||
|
||
@table
|
||
|> :ets.tab2list()
|
||
|> Enum.sort_by(fn {ts, _, _} -> DateTime.to_unix(ts) end)
|
||
|> Enum.take(excess)
|
||
|> Enum.each(fn {ts, _, _} -> :ets.delete(@table, ts) end)
|
||
end
|
||
end
|
||
|
||
@spec prune_older_than(DateTime.t()) :: non_neg_integer()
|
||
def prune_older_than(cutoff_ts) do
|
||
match_spec = [{{:"$1", :_, :_}, [{:<, :"$1", {:const, cutoff_ts}}], [true]}]
|
||
:ets.select_delete(@table, match_spec)
|
||
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, :compressed, read_concurrency: true])
|
||
{:ok, %{}}
|
||
end
|
||
end
|