aprs.me/lib/aprsme/weather_cache.ex
Graham McIntire 281aef095c
fix: resolve all credo warnings and pre-existing dialyzer errors
- Move require Logger to module top level in SpatialPubSub
- Replace weak is_list assertions with stronger checks
- Remove redundant 'is a list' test in PromEx tests
- Replace apply/3 with direct function call in packet_replay test
- Fix unmatched_return in weather_cache.ex and callsign_view.ex
2026-07-17 16:41:15 -05:00

90 lines
2.1 KiB
Elixir

defmodule Aprsme.WeatherCache do
@moduledoc """
Persistent ETS-based cache for weather callsign lookups.
Eliminates N+1 queries for `has_weather_packets?` checks during map
marker rendering. Entries expire after `@ttl_ms` to stay reasonably
current without per-callsign DB queries.
"""
@cache_name :weather_cache
@ttl_ms to_timeout(minute: 5)
@doc """
Create the ETS table if it doesn't exist yet.
The table is typically created in application.ex alongside other ETS tables.
"""
def setup do
_table =
if :ets.whereis(@cache_name) == :undefined do
:ets.new(@cache_name, [
:named_table,
:public,
:set,
read_concurrency: true,
write_concurrency: true
])
end
:ok
rescue
ArgumentError -> :ok
end
@doc """
Batch-populate the cache for the given list of callsigns.
"""
def cache_weather_callsigns(callsigns) when is_list(callsigns) do
weather_set = Aprsme.Packets.weather_callsigns(callsigns)
expires_at = System.monotonic_time(:millisecond) + @ttl_ms
Enum.each(callsigns, fn cs ->
key = String.upcase(String.trim(cs))
has_weather = MapSet.member?(weather_set, key)
:ets.insert(@cache_name, {key, has_weather, expires_at})
end)
:ok
rescue
_ -> :ok
end
@doc """
Check if a callsign has weather packets.
Returns `:unknown` if not cached, `true`/`false` if cached.
"""
def weather_callsign?(callsign) when is_binary(callsign) do
key = String.upcase(String.trim(callsign))
case :ets.lookup(@cache_name, key) do
[{^key, value, expires_at}] ->
if System.monotonic_time(:millisecond) < expires_at do
value
else
:ets.delete(@cache_name, key)
:unknown
end
_ ->
:unknown
end
end
@doc """
Evict expired entries.
"""
def evict_expired do
now = System.monotonic_time(:millisecond)
@cache_name
|> :ets.tab2list()
|> Enum.each(fn
{key, _value, expires_at} when expires_at < now ->
:ets.delete(@cache_name, key)
_ ->
:ok
end)
:ok
end
end