defmodule AbsintheCacheFairy.ConCacheProvider do
@moduledoc ~s"""
Implements Sanbase.Cache.Behaviour for con_cache
"""
# TODO: Check if con_cache is available
@behaviour AbsintheCacheFairy.Behaviour
@compile {:inline,
get: 2,
store: 3,
get_or_store: 4,
cache_item: 3,
get_or_store_isolated: 5,
execute_and_maybe_cache_function: 4}
@max_cache_ttl 86_400
@impl true
def size(cache, :megabytes) do
bytes_size = :ets.info(ConCache.ets(cache), :memory) * :erlang.system_info(:wordsize)
(bytes_size / (1024 * 1024)) |> Float.round(2)
end
@impl true
def clear_all(cache) do
cache
|> ConCache.ets()
|> :ets.tab2list()
|> Enum.each(fn {key, _} -> ConCache.delete(cache, key) end)
end
@impl true
def get(cache, key) do
case ConCache.get(cache, true_key(key)) do
{:stored, value} -> value
nil -> nil
end
end
@impl true
def store(cache, key, value) do
case value do
{:error, _} ->
:ok
{:nocache, _} ->
Process.put(:has_nocache_field, true)
:ok
value ->
cache_item(cache, key, {:stored, value})
end
end
@impl true
def get_or_store(cache, key, func, cache_modify_middleware) do
# Do not include the TTL as part of the key name.
true_key = true_key(key)
case ConCache.get(cache, true_key) do
{:stored, value} ->
value
_ ->
get_or_store_isolated(cache, key, true_key, func, cache_modify_middleware)
end
end
defp get_or_store_isolated(cache, key, true_key, func, middleware_func) do
# This function is to be executed inside ConCache.isolated/3 call.
# This isolated call locks the access for that key before doing anything else
# Doing this ensures that the case where another process modified the key
# before in the time between the previous check and the locking.
fun = fn ->
case ConCache.get(cache, true_key) do
{:stored, value} ->
value
_ ->
execute_and_maybe_cache_function(
cache,
key,
func,
middleware_func
)
end
end
ConCache.isolated(cache, true_key, fun)
end
defp execute_and_maybe_cache_function(cache, key, func, middleware_func) do
# Execute the function and if it returns :ok tuple cache it
# Errors are not cached. Also, caching can be manually disabled by
# wrapping the result in a :nocache tuple
case func.() do
{:error, _} = error ->
error
{:middleware, _, _} = tuple ->
# Decides on its behalf whether or not to put the value in the cache
middleware_func.(cache, key, tuple)
{:nocache, {:ok, _result} = value} ->
Process.put(:do_not_cache_query, true)
value
value ->
cache_item(cache, key, {:stored, value})
value
end
end
defp cache_item(cache, {key, ttl}, value) when is_integer(ttl) and ttl <= @max_cache_ttl do
ConCache.put(cache, key, %ConCache.Item{
value: value,
ttl: :timer.seconds(ttl)
})
end
defp cache_item(cache, key, value) do
ConCache.put(cache, key, value)
end
defp true_key({key, ttl}) when is_integer(ttl) and ttl <= @max_cache_ttl, do: key
defp true_key(key), do: key
end