# 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