Elixir by Example

The store becomes a process

Builds on GenServer

Edit any cell and run it in your browser Download source

Steps 1-3 built a store that lives in a variable. To share it between processes it has to live in a process. A GenServer holds the buckets as its state, and every caller talks to it by name.

The pure data code doesn't change. The GenServer is a thin shell around it, which keeps the logic easy to test without processes.

defmodule KV.Data do
  def put(b, bucket, key, value),
    do: Map.update(b, bucket, %{key => value}, &Map.put(&1, key, value))

  def get(b, bucket, key), do: b |> Map.get(bucket, %{}) |> Map.fetch(key)
  def delete(b, bucket, key), do: Map.update(b, bucket, %{}, &Map.delete(&1, key))
end

defmodule KV.Server do
  use GenServer

  def start_link(opts), do: GenServer.start_link(__MODULE__, %{}, name: opts[:name] || __MODULE__)

  # Writes are calls too: the caller learns the write
  # happened before it continues, so a read right after
  # a write always sees it.
  def put(bucket, key, value), do: GenServer.call(__MODULE__, {:put, bucket, key, value})
  def get(bucket, key), do: GenServer.call(__MODULE__, {:get, bucket, key})
  def delete(bucket, key), do: GenServer.call(__MODULE__, {:delete, bucket, key})

  @impl true
  def init(buckets), do: {:ok, buckets}

  @impl true
  def handle_call({:put, b, k, v}, _from, s), do: {:reply, :ok, KV.Data.put(s, b, k, v)}
  def handle_call({:get, b, k}, _from, s), do: {:reply, KV.Data.get(s, b, k), s}
  def handle_call({:delete, b, k}, _from, s), do: {:reply, :ok, KV.Data.delete(s, b, k)}
end

{:ok, _} = KV.Server.start_link([])

KV.Server.put("users", "ada", :admin)
IO.inspect(KV.Server.get("users", "ada"), label: "get")
get: {:ok, :admin}

Now many processes share one store. 50 tasks write concurrently; the GenServer applies them one at a time, so none are lost.

1..50
|> Enum.map(fn i -> Task.async(fn -> KV.Server.put("counters", "k#{i}", i) end) end)
|> Task.await_many()

{:ok, 50} = KV.Server.get("counters", "k50")
IO.inspect(KV.Server.get("counters", "k7"), label: "written by another process")

KV.Server.delete("users", "ada")
IO.inspect(KV.Server.get("users", "ada"), label: "after delete")
written by another process: {:ok, 7}
after delete: :error

Two problems remain. One crash in any bucket takes the whole store down, and one busy bucket slows down all the others because a single process serves everything. Step 5 gives each bucket its own supervised process.