perf: add persistent ETS weather cache for N+1 prevention

Ultraworked with Sisyphus

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Graham McIntire 2026-06-01 16:48:28 -05:00
parent 3948ae5894
commit 21ab9fb20c
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
2 changed files with 94 additions and 0 deletions

View file

@ -197,6 +197,7 @@ defmodule Aprsme.Application do
_ = :ets.new(:query_cache, [:set, :public, :named_table, read_concurrency: true, write_concurrency: true])
_ = :ets.new(:device_cache, [:set, :public, :named_table, read_concurrency: true, write_concurrency: true])
_ = :ets.new(:symbol_cache, [:set, :public, :named_table, read_concurrency: true, write_concurrency: true])
_ = :ets.new(:weather_cache, [:set, :public, :named_table, read_concurrency: true, write_concurrency: true])
_ = :ets.new(:aprsme, [:set, :public, :named_table, read_concurrency: true, write_concurrency: true])
_ = :ets.insert(:aprsme, {:message_number, 0})

View file

@ -0,0 +1,93 @@
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
_ =
case :ets.info(@cache_name) do
:undefined ->
:ets.new(@cache_name, [
:named_table,
:public,
:set,
:protected,
read_concurrency: true,
write_concurrency: true
])
_ ->
:ok
end
: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