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:
parent
3948ae5894
commit
21ab9fb20c
2 changed files with 94 additions and 0 deletions
|
|
@ -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})
|
||||
|
||||
|
|
|
|||
93
lib/aprsme/weather_cache.ex
Normal file
93
lib/aprsme/weather_cache.ex
Normal 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
|
||||
Loading…
Add table
Reference in a new issue