fix(cache): add periodic TTL sweeper to bound ETS growth
This commit is contained in:
parent
5255a6ba63
commit
b874020a87
2 changed files with 64 additions and 0 deletions
|
|
@ -11,6 +11,7 @@ defmodule Microwaveprop.Cache do
|
|||
use GenServer
|
||||
|
||||
@table :microwaveprop_cache
|
||||
@sweep_interval_ms to_timeout(minute: 1)
|
||||
|
||||
@spec start_link(keyword()) :: GenServer.on_start()
|
||||
def start_link(opts) do
|
||||
|
|
@ -56,6 +57,25 @@ defmodule Microwaveprop.Cache do
|
|||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Delete every entry whose TTL has elapsed. Returns the number of rows
|
||||
removed.
|
||||
|
||||
Because TTL enforcement in `fetch_or_store/3` is lazy, keys minted
|
||||
dynamically (e.g. per-URL HRRR idx entries) would otherwise accumulate
|
||||
forever. A supervised timer calls this on an interval; tests can call
|
||||
it directly to avoid waiting on the timer.
|
||||
|
||||
Uses `:ets.select_delete/2` so the sweep happens in a single BIF call
|
||||
without blocking concurrent readers or writers.
|
||||
"""
|
||||
@spec sweep() :: non_neg_integer()
|
||||
def sweep do
|
||||
now = System.monotonic_time(:millisecond)
|
||||
match_spec = [{{:_, :_, :"$1"}, [{:"=<", :"$1", now}], [true]}]
|
||||
:ets.select_delete(@table, match_spec)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_opts) do
|
||||
_ =
|
||||
|
|
@ -67,6 +87,18 @@ defmodule Microwaveprop.Cache do
|
|||
write_concurrency: true
|
||||
])
|
||||
|
||||
schedule_sweep()
|
||||
{:ok, %{}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:sweep, state) do
|
||||
_ = sweep()
|
||||
schedule_sweep()
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
defp schedule_sweep do
|
||||
Process.send_after(self(), :sweep, @sweep_interval_ms)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -43,4 +43,36 @@ defmodule Microwaveprop.CacheTest do
|
|||
assert Cache.fetch_or_store(:k, 1_000, fn -> "fresh" end) == "fresh"
|
||||
end
|
||||
end
|
||||
|
||||
describe "sweep/0" do
|
||||
test "removes expired entries" do
|
||||
Cache.put(:expired, "old", 1)
|
||||
Process.sleep(5)
|
||||
|
||||
assert Cache.sweep() >= 1
|
||||
assert :ets.lookup(:microwaveprop_cache, :expired) == []
|
||||
end
|
||||
|
||||
test "leaves unexpired entries in place" do
|
||||
Cache.put(:fresh, "new", 60_000)
|
||||
|
||||
_ = Cache.sweep()
|
||||
|
||||
assert [{:fresh, "new", _}] = :ets.lookup(:microwaveprop_cache, :fresh)
|
||||
end
|
||||
|
||||
test "handles a mix of expired and unexpired entries" do
|
||||
Cache.put(:expired_a, 1, 1)
|
||||
Cache.put(:expired_b, 2, 1)
|
||||
Cache.put(:fresh, 3, 60_000)
|
||||
Process.sleep(5)
|
||||
|
||||
deleted = Cache.sweep()
|
||||
assert deleted >= 2
|
||||
|
||||
assert :ets.lookup(:microwaveprop_cache, :expired_a) == []
|
||||
assert :ets.lookup(:microwaveprop_cache, :expired_b) == []
|
||||
assert [{:fresh, 3, _}] = :ets.lookup(:microwaveprop_cache, :fresh)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue