defmodule Kdb.Cache do defstruct [:name, :ttl, :t] @type t :: %__MODULE__{ name: atom(), ttl: integer(), t: any() } @unit_time :millisecond @key_delete :delete def new(opts) do name = Keyword.get(opts, :name) || make_ref() case Kdb.Registry.get_cache(name) do nil -> t = :ets.new(__MODULE__, [ :set, :public, read_concurrency: true, write_concurrency: true ]) ttl = Keyword.get(opts, :ttl, :undefined) cache = %__MODULE__{name: name, ttl: ttl, t: t} public = Keyword.get(opts, :public, true) if public do Kdb.Registry.register(cache) end cache cache -> cache end end @spec put( cache :: t(), bucket :: atom(), key :: binary(), value :: term(), bucket_ttl :: integer() | :infinity ) :: boolean() def put(%__MODULE__{t: t, ttl: ttl}, bucket, key, value, bucket_ttl) do :ets.insert(t, {{bucket, key}, value, calc_ttl(ttl, bucket_ttl)}) end @spec update_counter( cache :: t(), bucket_name :: atom(), key :: binary(), amount :: integer(), default :: term(), bucket_ttl :: integer() | :infinity ) :: integer() def update_counter(%__MODULE__{t: t, ttl: ttl}, bucket_name, key, amount, default, bucket_ttl) do id = {bucket_name, key} :ets.update_counter(t, id, {2, amount}, {id, default, calc_ttl(ttl, bucket_ttl)}) end @spec get( cache :: t(), bucket_name :: atom(), key :: binary(), bucket_ttl :: integer() | :infinity ) :: term() | nil def get(%__MODULE__{t: t, ttl: ttl}, bucket, key, bucket_ttl) do id = {bucket, key} case :ets.lookup(t, id) do [{_key, @key_delete, _timestamp}] -> @key_delete [{_key, value, _timestamp}] -> update_ttl(t, id, calc_ttl(ttl, bucket_ttl)) value _ -> nil end end def has_key?(%__MODULE__{t: t}, bucket, key) do case :ets.lookup(t, {bucket, key}) do [{_key, @key_delete, _timestamp}] -> false [{_key, _value, _timestamp}] -> true _ -> nil end end @spec update( cache :: t(), bucket :: atom(), key :: binary(), value :: term(), default :: term(), bucket_ttl :: integer() | :infinity ) :: boolean() def update(%__MODULE__{t: t, ttl: ttl}, bucket, key, value, default, bucket_ttl) do :ets.update_element( t, {bucket, key}, {2, value}, {{bucket, key}, default, calc_ttl(ttl, bucket_ttl)} ) end @spec delete(cache :: t(), atom(), binary()) :: true def delete(%__MODULE__{t: t}, bucket, id) do # :ets.delete(t, {bucket, id}) :ets.insert(t, {{bucket, id}, @key_delete, 0}) end def cleanup(older_than) do tid = :ets.whereis(Kdb.Registry) :ets.foldl( fn {{:batch, _batch_id}, %{cache: cache}}, acc -> if cache.ttl != :infinity do acc + cleanup(cache, older_than) else acc end _, acc -> acc end, 0, tid ) end @spec cleanup(batch :: any(), older_than :: integer()) :: integer() def cleanup(%Kdb.Cache{t: tid}, older_than) do n = :ets.foldl( fn {key, @key_delete, _}, acc -> :ets.delete(tid, key) acc + 1 {key, _value, readed_at}, acc when is_integer(readed_at) and readed_at < older_than -> :ets.delete(tid, key) acc + 1 _, acc -> acc end, 0, tid ) n end defp calc_ttl(:infinity, _b), do: :infinity defp calc_ttl(:undefined, :infinity), do: :infinity defp calc_ttl(:undefined, ttl), do: now_add(ttl) defp calc_ttl(ttl, _), do: now_add(ttl) defp now_add(ttl) do :os.system_time(@unit_time) + ttl end defp update_ttl(_t, _id, ttl) when is_atom(ttl), do: :ok defp update_ttl(t, id, ttl) do :ets.update_element(t, id, {3, now_add(ttl)}) end end