Current section
Files
Jump to
Current section
Files
lib/lotus/cache/ets.ex
defmodule Lotus.Cache.ETS do
@moduledoc """
An ETS-based, local, in-memory cache adapter for Lotus.
This is the default cache adapter if none is specified.
If you want a distributed cache, consider using `Lotus.Cache.Cachex`.
"""
use Lotus.Cache.Adapter
@table :lotus_cache
@tag_table :lotus_cache_tags
def child_spec(_opts \\ []) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [[]]},
type: :supervisor
}
end
def start_link(_opts) do
ensure_tables!()
start_janitor()
{:ok, self()}
end
@impl Lotus.Cache.Adapter
def spec_config do
[{Lotus.Cache.ETS, []}]
end
@impl Lotus.Cache.Adapter
def get(key) do
case :ets.lookup(@table, key) do
[{^key, value_bin, expires_at}] ->
if expired?(expires_at) do
:ets.delete(@table, key)
:miss
else
{:ok, decode(value_bin)}
end
_ ->
:miss
end
end
@impl Lotus.Cache.Adapter
def put(key, value, ttl_ms, opts) do
compress = Keyword.get(opts, :compress, true)
encoded = encode(value, compress)
max_bytes = Keyword.get(opts, :max_bytes, 5_000_000)
if byte_size(encoded) > max_bytes, do: :ok, else: do_put(key, encoded, ttl_ms, opts)
end
defp do_put(key, bin, ttl_ms, opts) do
expires_at = now_ms() + ttl_ms
:ets.insert(@table, {key, bin, expires_at})
for t <- Keyword.get(opts, :tags, []), do: :ets.insert(@tag_table, {t, key})
:ok
end
@impl Lotus.Cache.Adapter
def delete(key) do
:ets.delete(@table, key)
:ok
end
@impl Lotus.Cache.Adapter
def touch(key, ttl_ms) do
case :ets.lookup(@table, key) do
[{^key, v, _old}] ->
:ets.insert(@table, {key, v, now_ms() + ttl_ms})
:ok
_ ->
:ok
end
end
@impl Lotus.Cache.Adapter
def get_or_store(key, ttl_ms, fun, opts) do
case get(key) do
{:ok, v} ->
{:ok, v, :hit}
:miss ->
lock = {:lotus_cache_lock, key}
case :global.set_lock(lock, [node()], Keyword.get(opts, :lock_timeout, 10_000)) do
true ->
try do
case get(key) do
{:ok, v} ->
{:ok, v, :hit}
:miss ->
value = fun.()
put(key, value, ttl_ms, opts)
{:ok, value, :miss}
end
after
:global.del_lock(lock)
end
false ->
value = fun.()
{:ok, value, :miss}
end
end
end
@impl Lotus.Cache.Adapter
def invalidate_tags(tags) do
for tag <- tags do
for {^tag, key} <- :ets.lookup(@tag_table, tag) do
:ets.delete(@table, key)
:ets.delete_object(@tag_table, {tag, key})
end
end
:ok
end
defp ensure_tables! do
unless :ets.whereis(@table) != :undefined do
:ets.new(@table, [
:set,
:named_table,
:public,
read_concurrency: true,
write_concurrency: true
])
end
unless :ets.whereis(@tag_table) != :undefined do
:ets.new(@tag_table, [:bag, :named_table, :public, write_concurrency: true])
end
end
defp start_janitor do
spawn_link(fn -> janitor_loop() end)
end
defp janitor_loop do
Process.sleep(:timer.seconds(30))
now = now_ms()
for {key, _v, expires_at} <- :ets.tab2list(@table), expires_at <= now do
:ets.delete(@table, key)
end
janitor_loop()
end
defp expired?(ts), do: ts <= now_ms()
defp now_ms, do: System.monotonic_time(:millisecond)
end