155 lines
4.3 KiB
Elixir
155 lines
4.3 KiB
Elixir
# original source: https://github.com/nikolaostoji/elixir-lru-cache/blob/47742fe1f49a75a08ef4a4146373d762348b6546/lib/lru_cache.ex
|
|
defmodule Floof.LruCache do
|
|
use GenServer
|
|
defstruct capacity: 0
|
|
|
|
### Client API
|
|
|
|
def start_link(opts) do
|
|
opts2 = Map.take(opts, [:capacity, :position_table, :cache_table])
|
|
GenServer.start_link(__MODULE__, opts2, name: opts.name)
|
|
end
|
|
|
|
@doc """
|
|
Creates a LRU cache
|
|
## Examples
|
|
iex> Floof.LruCache.create()
|
|
"""
|
|
def create(capacity \\ 5, name \\ __MODULE__) when is_integer(capacity) do
|
|
GenServer.start_link(__MODULE__, %{:capacity => capacity}, name: name)
|
|
end
|
|
|
|
@doc """
|
|
Adds a kvp to the cache. If the cache capacity exceeds, removes
|
|
the least recently used key. If the key does not existIf key
|
|
already exists, updates the value and returns false.
|
|
## Parameters
|
|
- key: key
|
|
- value: value
|
|
## Examples
|
|
iex> Floof.LruCache.put(:LruCache, "test-key", 123)
|
|
true
|
|
"""
|
|
def put(server, key, value) do
|
|
GenServer.call(server, {:put, key, value})
|
|
end
|
|
|
|
@doc """
|
|
Gets value if key is present, returns nil otherwise.
|
|
## Parameters
|
|
- key: key
|
|
## Examples
|
|
iex> Floof.LruCache.get(:LruCache, "key-exists")
|
|
"""
|
|
def get(server, key) do
|
|
GenServer.call(server, {:get, key})
|
|
end
|
|
|
|
def get_multi(server, keys) do
|
|
GenServer.call(server, {:getMulti, keys})
|
|
end
|
|
|
|
@doc """
|
|
Removes a kvp from the cache.
|
|
## Parameters
|
|
- key: key
|
|
## Examples
|
|
iex> Floof.LruCache.delete(:LruCache, "key")
|
|
true
|
|
"""
|
|
def delete(server, key) do
|
|
GenServer.call(server, {:delete, key})
|
|
end
|
|
|
|
@doc """
|
|
Removes all entries from the cache.
|
|
"""
|
|
def clear(server) do
|
|
GenServer.call(server, :clear)
|
|
end
|
|
|
|
### Server Callbacks
|
|
|
|
def init(opts) do
|
|
opts =
|
|
Map.merge(
|
|
%{:capacity => 5, :position_table => :position_table, :cache_table => :cache_table},
|
|
opts
|
|
)
|
|
|
|
# kvp = {key: key, value: key_time }
|
|
:ets.new(opts.position_table, [:named_table, :public])
|
|
# kvp = {key: key_time, time: value}
|
|
:ets.new(opts.cache_table, [:named_table, :ordered_set])
|
|
{:ok, opts}
|
|
end
|
|
|
|
def handle_call({:put, key, value}, _from, lru_state) do
|
|
wasUpdated = insert_kvp(lru_state, key, value)
|
|
remove_least_recently_used(lru_state)
|
|
{:reply, wasUpdated, lru_state}
|
|
end
|
|
|
|
def handle_call({:get, key}, _from, lru_state) do
|
|
{:reply,
|
|
case :ets.lookup(lru_state.position_table, key) do
|
|
[{_, time_key}] -> update_item_position(lru_state, key, time_key)
|
|
[] -> nil
|
|
end, lru_state}
|
|
end
|
|
|
|
def handle_call({:getMulti, keys}, _from, lru_state) do
|
|
position_table = lru_state.position_table
|
|
|
|
{:reply,
|
|
Enum.map(keys, fn key ->
|
|
{key,
|
|
case :ets.lookup(position_table, key) do
|
|
[{_, time_key}] -> update_item_position(lru_state, key, time_key)
|
|
[] -> nil
|
|
end}
|
|
end), lru_state}
|
|
end
|
|
|
|
def handle_call({:delete, key}, _from, lru_state) do
|
|
result = :ets.delete(lru_state.position_table, key)
|
|
:ets.delete(lru_state.cache_table, key)
|
|
{:reply, result, lru_state}
|
|
end
|
|
|
|
def handle_call(:clear, _from, lru_state) do
|
|
:ets.delete_all_objects(lru_state.position_table)
|
|
:ets.delete_all_objects(lru_state.cache_table)
|
|
{:reply, nil, lru_state}
|
|
end
|
|
|
|
defp remove_least_recently_used(lru_state) do
|
|
# if we exceed capacity remove least recently used item
|
|
num_items = :ets.info(lru_state.position_table, :size)
|
|
|
|
if num_items > lru_state.capacity do
|
|
time_key = :ets.first(lru_state.cache_table)
|
|
[{_, {key, _val}}] = :ets.lookup(lru_state.cache_table, time_key)
|
|
:ets.delete(lru_state.cache_table, time_key)
|
|
:ets.delete(lru_state.position_table, key)
|
|
end
|
|
end
|
|
|
|
defp update_item_position(lru_state, key, time_key) do
|
|
# Puts item in back of table
|
|
counter = :erlang.unique_integer([:monotonic])
|
|
cache_table = lru_state.cache_table
|
|
[{_, {_key, val}}] = :ets.lookup(cache_table, time_key)
|
|
:ets.delete(cache_table, time_key)
|
|
:ets.insert(cache_table, {counter, {key, val}})
|
|
:ets.insert(lru_state.position_table, {key, counter})
|
|
val
|
|
end
|
|
|
|
defp insert_kvp(lru_state, key, value) do
|
|
counter = :erlang.unique_integer([:monotonic])
|
|
wasUpdated = :ets.insert(lru_state.position_table, {key, counter})
|
|
:ets.insert(lru_state.cache_table, {counter, {key, value}})
|
|
wasUpdated
|
|
end
|
|
end
|