Packages

High performance embedded realtime database combining ETS with TTL for caching, RocksDB for persistent parallel writes, and SQLite for secondary indexing and fast lookups

Current section

Files

Jump to
kdb lib cache.ex
Raw

lib/cache.ex

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