Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
177 lines (139 sloc) 4.54 KB
defmodule Crux.Cache.Base do
@moduledoc """
Base cache utilising ETS tables, `:ets`
"""
alias Crux.Structs
alias Crux.Structs.Util
defmacro __using__(args \\ []) do
# This does not feel right
quote location: :keep do
use GenServer
@behaviour Crux.Cache
@name __MODULE__
@struct unquote(args[:struct])
@spec start_link(args :: term) :: GenServer.on_start()
def start_link(args \\ []), do: GenServer.start_link(__MODULE__, %{}, name: @name)
@spec insert(data :: term()) :: term()
def insert(data), do: Crux.Cache.Base.cache(@name, data)
@spec update(data :: term()) :: term()
def update(data), do: Crux.Cache.Base.update(@name, data)
@spec delete(id :: Crux.Cache.key()) :: :ok
def delete(id), do: Crux.Cache.Base.delete(@name, id)
@spec fetch(id :: Crux.Cache.key()) :: {:ok, term()} | :error
@spec fetch!(id :: identifier()) :: {:ok, term()} | :error
if @struct do
def fetch(id) do
with {:ok, data} <- Crux.Cache.Base.fetch(@name, id),
do: {:ok, Structs.create(data, @struct)}
end
def fetch!(id) do
@name
|> Crux.Cache.Base.fetch!(id)
|> Structs.create(@struct)
end
else
def fetch(id), do: Crux.Cache.Base.fetch(@name, id)
def fetch!(id), do: Crux.Cache.Base.fetch!(@name, id)
end
@doc false
@impl true
def init(args) do
:ets.new(@name, [:named_table, read_concurrency: true])
{:ok, args}
end
@doc false
@impl true
def handle_cast({:cache, structure}, state) do
{_, _, state} = handle_call({:update, structure}, nil, state)
{:noreply, state}
end
@doc false
@impl true
def handle_call({:update, structure}, _from, state) do
structure =
case Crux.Cache.Base.fetch(@name, structure.id) do
{:ok, oldstruct} ->
structure = Crux.Cache.Base.deep_merge(oldstruct, structure)
:error ->
structure
end
:ets.insert(@name, {structure.id, structure})
{:reply, structure, state}
end
@doc false
def handle_call({:delete, id}, _from, state) do
:ets.delete(@name, id)
{:reply, :ok, state}
end
defoverridable start_link: 1,
insert: 1,
update: 1,
delete: 1,
fetch: 1,
fetch!: 1,
init: 1,
handle_call: 3
end
end
@doc false
@spec cache(GenServer.server(), map() | struct()) :: map() | struct()
def cache(name, structure) do
structure =
structure
|> Util.atomify()
|> Map.update!(:id, &Util.id_to_int/1)
GenServer.cast(name, {:cache, structure})
structure
end
@doc false
@spec update(GenServer.server(), map() | struct()) :: map() | struct()
def update(name, structure) do
structure =
structure
|> Util.atomify()
|> Map.update!(:id, &Util.id_to_int/1)
GenServer.call(name, {:update, structure})
end
@doc false
@spec fetch(GenServer.server(), Crux.Cache.key()) :: {:ok, struct()} | :error
def fetch(name, id) do
case :ets.lookup(name, id) do
[{^id, structure}] ->
{:ok, structure}
_ ->
:error
end
end
@doc false
@spec fetch!(GenServer.server(), Crux.Cache.key()) :: struct() | no_return()
def fetch!(name, id) do
case fetch(name, id) do
{:ok, structure} ->
structure
_ ->
raise "Could not find #{inspect(id)} in #{name}"
end
end
@doc false
@spec delete(GenServer.server(), Crux.Cache.key()) :: :ok
def delete(name, id) do
case fetch(name, id) do
{:ok, _} ->
GenServer.call(name, {:delete, id})
:error ->
:ok
end
end
@doc false
@spec deep_merge(term(), term()) :: term()
def deep_merge(%{} = old, %{} = new), do: Map.merge(old, new, &deep_merge_map/3)
# Replacing lists seems to be the correct way to "update" here
def deep_merge(old, new) when is_list(old) and is_list(new), do: new
def deep_merge(_old, new), do: new
@doc false
@spec deep_merge_map(term(), term(), term()) :: term()
def deep_merge_map(_key, %{} = old_v, new_v) when is_list(new_v), do: deep_merge(old_v, new_v)
# Replacing lists seems to be the correct way to "update" here
def deep_merge_map(_key, old_v, new_v) when is_list(old_v) and is_list(new_v), do: new_v
def deep_merge_map(_key, _oldV, new_v), do: new_v
end
You can’t perform that action at this time.