199 lines
4.8 KiB
Elixir
199 lines
4.8 KiB
Elixir
defmodule Aprsme.Cache do
|
|
@moduledoc """
|
|
Cache abstraction layer using ETS (Erlang Term Storage).
|
|
Provides a unified API for in-memory caching with optional TTL support.
|
|
|
|
Entries are stored as `{key, value, expires_at}` tuples where `expires_at`
|
|
is a monotonic time in milliseconds, or `:infinity` for no expiration.
|
|
Expired entries are lazily evicted on read.
|
|
"""
|
|
|
|
use GenServer
|
|
|
|
def start_link(opts) do
|
|
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
|
end
|
|
|
|
@impl true
|
|
def init(_opts) do
|
|
{:ok, %{}}
|
|
end
|
|
|
|
@doc """
|
|
Get a value from cache. Returns `{:ok, nil}` for expired entries.
|
|
"""
|
|
def get(cache_name, key) do
|
|
GenServer.call(__MODULE__, {:get, cache_name, key})
|
|
end
|
|
|
|
@doc """
|
|
Put a value in cache with optional TTL.
|
|
|
|
## Options
|
|
* `:ttl` - Time to live in milliseconds. Use `Cache.to_timeout/1` for convenience.
|
|
"""
|
|
def put(cache_name, key, value, opts \\ []) do
|
|
GenServer.call(__MODULE__, {:put, cache_name, key, value, opts})
|
|
end
|
|
|
|
@doc """
|
|
Delete a key from cache
|
|
"""
|
|
def del(cache_name, key) do
|
|
GenServer.call(__MODULE__, {:del, cache_name, key})
|
|
end
|
|
|
|
@doc """
|
|
Clear all keys from cache
|
|
"""
|
|
def clear(cache_name) do
|
|
GenServer.call(__MODULE__, {:clear, cache_name})
|
|
end
|
|
|
|
@doc """
|
|
Get cache statistics (simplified for ETS)
|
|
"""
|
|
def stats(cache_name) do
|
|
GenServer.call(__MODULE__, {:stats, cache_name})
|
|
end
|
|
|
|
@doc """
|
|
Check if key exists and is not expired.
|
|
"""
|
|
def exists?(cache_name, key) do
|
|
case get(cache_name, key) do
|
|
{:ok, nil} -> false
|
|
{:ok, _} -> true
|
|
_ -> false
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Get TTL for a key. Returns remaining milliseconds or nil if no TTL.
|
|
"""
|
|
def ttl(cache_name, key) do
|
|
GenServer.call(__MODULE__, {:ttl, cache_name, key})
|
|
end
|
|
|
|
@doc """
|
|
Convert timeout keyword list to milliseconds.
|
|
"""
|
|
def to_timeout(opts) do
|
|
Enum.reduce(opts, 0, fn
|
|
{:second, n}, acc -> acc + n * 1000
|
|
{:seconds, n}, acc -> acc + n * 1000
|
|
{:minute, n}, acc -> acc + n * 60 * 1000
|
|
{:minutes, n}, acc -> acc + n * 60 * 1000
|
|
{:hour, n}, acc -> acc + n * 60 * 60 * 1000
|
|
{:hours, n}, acc -> acc + n * 60 * 60 * 1000
|
|
{:day, n}, acc -> acc + n * 24 * 60 * 60 * 1000
|
|
{:days, n}, acc -> acc + n * 24 * 60 * 60 * 1000
|
|
{:millisecond, n}, acc -> acc + n
|
|
{:milliseconds, n}, acc -> acc + n
|
|
end)
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:get, cache_name, key}, _from, state) do
|
|
result =
|
|
try do
|
|
case :ets.lookup(cache_name, key) do
|
|
[{^key, value, :infinity}] ->
|
|
{:ok, value}
|
|
|
|
[{^key, value, expires_at}] ->
|
|
if System.monotonic_time(:millisecond) < expires_at do
|
|
{:ok, value}
|
|
else
|
|
:ets.delete(cache_name, key)
|
|
{:ok, nil}
|
|
end
|
|
|
|
[{^key, value}] ->
|
|
{:ok, value}
|
|
|
|
[] ->
|
|
{:ok, nil}
|
|
end
|
|
rescue
|
|
ArgumentError ->
|
|
{:error, :no_cache}
|
|
end
|
|
|
|
{:reply, result, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:put, cache_name, key, value, opts}, _from, state) do
|
|
result =
|
|
try do
|
|
expires_at =
|
|
case Keyword.get(opts, :ttl) do
|
|
nil -> :infinity
|
|
ttl when is_integer(ttl) and ttl > 0 -> System.monotonic_time(:millisecond) + ttl
|
|
_ -> :infinity
|
|
end
|
|
|
|
:ets.insert(cache_name, {key, value, expires_at})
|
|
{:ok, true}
|
|
rescue
|
|
ArgumentError -> {:error, :no_cache}
|
|
end
|
|
|
|
{:reply, result, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:del, cache_name, key}, _from, state) do
|
|
result =
|
|
try do
|
|
:ets.delete(cache_name, key)
|
|
{:ok, true}
|
|
rescue
|
|
ArgumentError -> {:error, :no_cache}
|
|
end
|
|
|
|
{:reply, result, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:clear, cache_name}, _from, state) do
|
|
result =
|
|
try do
|
|
:ets.delete_all_objects(cache_name)
|
|
{:ok, true}
|
|
rescue
|
|
ArgumentError -> {:error, :no_cache}
|
|
end
|
|
|
|
{:reply, result, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:stats, cache_name}, _from, state) do
|
|
result =
|
|
case :ets.info(cache_name) do
|
|
:undefined -> {:error, :no_cache}
|
|
info when is_list(info) -> {:ok, %{size: Keyword.get(info, :size, 0)}}
|
|
end
|
|
|
|
{:reply, result, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:ttl, cache_name, key}, _from, state) do
|
|
result =
|
|
try do
|
|
case :ets.lookup(cache_name, key) do
|
|
[{^key, _value, :infinity}] -> {:ok, nil}
|
|
[{^key, _value, expires_at}] -> {:ok, max(0, expires_at - System.monotonic_time(:millisecond))}
|
|
[{^key, _value}] -> {:ok, nil}
|
|
[] -> {:ok, nil}
|
|
end
|
|
rescue
|
|
ArgumentError -> {:ok, nil}
|
|
end
|
|
|
|
{:reply, result, state}
|
|
end
|
|
end
|