Elixir Project: Key-Value Store
A key-value store is a table that holds one value under each key. This store keeps its table in ETS and runs under a supervisor.
Put and get use keys
A value is stored under a key and read back with the same key. A missing key answers with an error.
defmodule Store do
use GenServer
def start_link(_arg) do
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
end
def init(_state) do
table = :ets.new(:kv_store, [:set, :public])
{:ok, %{table: table}}
end
def handle_call({:put, key, value}, _from, state) do
:ets.insert(state.table, {key, {value, :never}})
{:reply, :ok, state}
end
def handle_call({:put_expiring, key, value, mark}, _from, state) do
:ets.insert(state.table, {key, {value, mark}})
{:reply, :ok, state}
end
def handle_call({:get, key}, _from, state) do
result = case :ets.lookup(state.table, key) do
[{_key, {value, _mark}}] -> {:ok, value}
[] -> :error
end
{:reply, result, state}
end
def handle_call({:delete, key}, _from, state) do
:ets.delete(state.table, key)
{:reply, :ok, state}
end
def handle_call(:keys, _from, state) do
keys = :ets.tab2list(state.table) |> Enum.map(fn {key, _entry} -> key end) |> Enum.sort()
{:reply, keys, state}
end
def handle_call({:purge, limit}, _from, state) do
expired = :ets.tab2list(state.table) |> Enum.filter(fn {_key, {_value, mark}} -> is_integer(mark) and mark <= limit end) |> Enum.map(fn {key, _entry} -> key end)
Enum.each(expired, fn key -> :ets.delete(state.table, key) end)
{:reply, length(expired), state}
end
def handle_call(:dump, _from, state) do
entries = :ets.tab2list(state.table) |> Enum.map(fn {key, {value, _mark}} -> {key, value} end) |> Enum.sort()
{:reply, entries, state}
end
end
{:ok, _pid} = Store.start_link([])
GenServer.call(Store, {:put, "name", "Ana"})
GenServer.call(Store, {:put, "city", "Oslo"})
IO.inspect(GenServer.call(Store, {:get, "name"}))
IO.inspect(GenServer.call(Store, {:get, "age"}))
GenServer.stop(Store)Deleting removes a key
After a delete, the key is gone from the key list. The other keys stay in sorted order.
defmodule Store do
use GenServer
def start_link(_arg) do
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
end
def init(_state) do
table = :ets.new(:kv_store, [:set, :public])
{:ok, %{table: table}}
end
def handle_call({:put, key, value}, _from, state) do
:ets.insert(state.table, {key, {value, :never}})
{:reply, :ok, state}
end
def handle_call({:put_expiring, key, value, mark}, _from, state) do
:ets.insert(state.table, {key, {value, mark}})
{:reply, :ok, state}
end
def handle_call({:get, key}, _from, state) do
result = case :ets.lookup(state.table, key) do
[{_key, {value, _mark}}] -> {:ok, value}
[] -> :error
end
{:reply, result, state}
end
def handle_call({:delete, key}, _from, state) do
:ets.delete(state.table, key)
{:reply, :ok, state}
end
def handle_call(:keys, _from, state) do
keys = :ets.tab2list(state.table) |> Enum.map(fn {key, _entry} -> key end) |> Enum.sort()
{:reply, keys, state}
end
def handle_call({:purge, limit}, _from, state) do
expired = :ets.tab2list(state.table) |> Enum.filter(fn {_key, {_value, mark}} -> is_integer(mark) and mark <= limit end) |> Enum.map(fn {key, _entry} -> key end)
Enum.each(expired, fn key -> :ets.delete(state.table, key) end)
{:reply, length(expired), state}
end
def handle_call(:dump, _from, state) do
entries = :ets.tab2list(state.table) |> Enum.map(fn {key, {value, _mark}} -> {key, value} end) |> Enum.sort()
{:reply, entries, state}
end
end
{:ok, _pid} = Store.start_link([])
GenServer.call(Store, {:put, "a", 1})
GenServer.call(Store, {:put, "b", 2})
GenServer.call(Store, {:put, "c", 3})
GenServer.call(Store, {:delete, "b"})
IO.inspect(GenServer.call(Store, :keys))
GenServer.stop(Store)Purging removes expired entries only
Purging with a limit of 5 removes the entry marked 3. The entry marked 9 and the permanent entry stay.
defmodule Store do
use GenServer
def start_link(_arg) do
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
end
def init(_state) do
table = :ets.new(:kv_store, [:set, :public])
{:ok, %{table: table}}
end
def handle_call({:put, key, value}, _from, state) do
:ets.insert(state.table, {key, {value, :never}})
{:reply, :ok, state}
end
def handle_call({:put_expiring, key, value, mark}, _from, state) do
:ets.insert(state.table, {key, {value, mark}})
{:reply, :ok, state}
end
def handle_call({:get, key}, _from, state) do
result = case :ets.lookup(state.table, key) do
[{_key, {value, _mark}}] -> {:ok, value}
[] -> :error
end
{:reply, result, state}
end
def handle_call({:delete, key}, _from, state) do
:ets.delete(state.table, key)
{:reply, :ok, state}
end
def handle_call(:keys, _from, state) do
keys = :ets.tab2list(state.table) |> Enum.map(fn {key, _entry} -> key end) |> Enum.sort()
{:reply, keys, state}
end
def handle_call({:purge, limit}, _from, state) do
expired = :ets.tab2list(state.table) |> Enum.filter(fn {_key, {_value, mark}} -> is_integer(mark) and mark <= limit end) |> Enum.map(fn {key, _entry} -> key end)
Enum.each(expired, fn key -> :ets.delete(state.table, key) end)
{:reply, length(expired), state}
end
def handle_call(:dump, _from, state) do
entries = :ets.tab2list(state.table) |> Enum.map(fn {key, {value, _mark}} -> {key, value} end) |> Enum.sort()
{:reply, entries, state}
end
end
{:ok, _pid} = Store.start_link([])
GenServer.call(Store, {:put_expiring, "temp", "x", 3})
GenServer.call(Store, {:put_expiring, "later", "y", 9})
GenServer.call(Store, {:put, "perm", "z"})
IO.inspect(GenServer.call(Store, {:purge, 5}))
IO.inspect(GenServer.call(Store, :keys))
GenServer.stop(Store)Example
defmodule Store do
use GenServer
def start_link(_arg) do
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
end
def init(_state) do
table = :ets.new(:kv_store, [:set, :public])
{:ok, %{table: table}}
end
def handle_call({:put, key, value}, _from, state) do
:ets.insert(state.table, {key, {value, :never}})
{:reply, :ok, state}
end
def handle_call({:put_expiring, key, value, mark}, _from, state) do
:ets.insert(state.table, {key, {value, mark}})
{:reply, :ok, state}
end
def handle_call({:get, key}, _from, state) do
result = case :ets.lookup(state.table, key) do
[{_key, {value, _mark}}] -> {:ok, value}
[] -> :error
end
{:reply, result, state}
end
def handle_call({:delete, key}, _from, state) do
:ets.delete(state.table, key)
{:reply, :ok, state}
end
def handle_call(:keys, _from, state) do
keys = :ets.tab2list(state.table) |> Enum.map(fn {key, _entry} -> key end) |> Enum.sort()
{:reply, keys, state}
end
def handle_call({:purge, limit}, _from, state) do
expired = :ets.tab2list(state.table) |> Enum.filter(fn {_key, {_value, mark}} -> is_integer(mark) and mark <= limit end) |> Enum.map(fn {key, _entry} -> key end)
Enum.each(expired, fn key -> :ets.delete(state.table, key) end)
{:reply, length(expired), state}
end
def handle_call(:dump, _from, state) do
entries = :ets.tab2list(state.table) |> Enum.map(fn {key, {value, _mark}} -> {key, value} end) |> Enum.sort()
{:reply, entries, state}
end
end
{:ok, sup} = Supervisor.start_link([Store], strategy: :one_for_one)
GenServer.call(Store, {:put, "name", "Ana"})
GenServer.call(Store, {:put, "city", "Oslo"})
GenServer.call(Store, {:put_expiring, "draft", "v1", 2})
IO.inspect(GenServer.call(Store, {:purge, 5}))
IO.inspect(GenServer.call(Store, :dump))
Supervisor.stop(sup)Key points
- Put: putting a key stores a value under that key.
- Get: getting a key returns its value, or an error when the key is missing.
- Delete: deleting a key removes it from the table.
- Expiry: an expiring entry carries a plain number mark, not a clock time.
- Purge: purging removes every entry whose mark is at or below the given limit.
In this example, the store runs under a supervisor and holds two permanent entries plus one expiring entry. Purging with a limit of 5 removes the one expiring entry, and the dump shows the two remaining entries in key order. The supervisor is stopped at the end.
Note
the table lives inside the store process, so stopping the store removes the table.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
The store holds an entry marked 3, an entry marked 9, and a permanent entry. Purging with a limit of 5 removes how many entries?