Skip to content

Instantly share code, notes, and snippets.

@schmalz
Last active October 6, 2017 13:38
Show Gist options
  • Save schmalz/d8acd3ba0f1c8ceacf2a39a8fa325e97 to your computer and use it in GitHub Desktop.
Save schmalz/d8acd3ba0f1c8ceacf2a39a8fa325e97 to your computer and use it in GitHub Desktop.
Elixir in Action - Chapter 7
defmodule Todo.Cache do
use GenServer
@moduledoc """
A todo-list cache; maps todo-list names to their corresponding server PIDs.
"""
# Client API
@doc """
Start a cache.
"""
def start(), do: GenServer.start(Todo.Cache, nil)
@doc """
Retrieve the todo server PID associated with `name`, creating the server if necessary.
"""
def server_process(cache_pid, name), do: GenServer.call(cache_pid, {:server_process, name})
# Server Callbacks
def init(_) do
Todo.Database.start("./persist")
{:ok, %{}}
end
def handle_call({:server_process, name}, _, servers) do
case Map.fetch(servers, name) do
{:ok, server} -> {:reply, server, servers}
:error ->
{:ok, server} = Todo.Server.start(name)
{:reply, server, Map.put(servers, name, server)}
end
end
end
defmodule Todo.Database do
use GenServer
@moduledoc """
Persistence for todo-lists.
"""
@server_name :database_server
@worker_count 3
# Client API
@doc """
Start a persistence server, storing its data in `db_folder_path`.
"""
def start(db_folder_path), do: GenServer.start(__MODULE__, db_folder_path, name: @server_name)
@doc """
Store `data` under `key`.
"""
def store(key, data), do: GenServer.cast(@server_name, {:store, key, data})
@doc """
Retrieve the data stored under `key`.
"""
def get(key), do: GenServer.call(@server_name, {:get, key})
# Server Callbacks
def init(db_folder_path) do
File.mkdir_p(db_folder_path)
workers =
0..(@worker_count - 1)
|> Stream.map(fn(i) ->
{:ok, worker_pid} = Todo.DatabaseWorker.start(db_folder_path)
{i, worker_pid}
end)
|> Enum.into(%{})
{:ok, workers}
end
def handle_cast({:store, key, data}, workers) do
Todo.DatabaseWorker.store(worker_for_key(workers, key), key, data)
{:noreply, workers}
end
def handle_call({:get, key}, _from, workers) do
data = Todo.DatabaseWorker.get(worker_for_key(workers, key), key)
{:reply, data, workers}
end
defp worker_for_key(workers, key), do: workers[:erlang.phash2(key, @worker_count)]
end
defmodule Todo.DatabaseWorker do
use GenServer
@moduledoc """
Worker for the todo-list persistence database server.
"""
# Client API
@doc """
Start a worker, storing its data in `db_folder_path`.
"""
def start(db_folder_path), do: GenServer.start(__MODULE__, db_folder_path)
@doc """
Store `data` under `key`.
"""
def store(worker_pid, key, data), do: GenServer.cast(worker_pid, {:store, key, data})
@doc """
Retrieve the data stored under `key`.
"""
def get(worker_pid, key), do: GenServer.call(worker_pid, {:get, key})
# Server Callbacks
def init(db_folder_path) do
{:ok, db_folder_path}
end
def handle_cast({:store, key, data}, db_folder_path) do
file_name(db_folder_path, key)
|> File.write!(:erlang.term_to_binary(data))
{:noreply, db_folder_path}
end
def handle_call({:get, key}, _from, db_folder_path) do
data =
case File.read(file_name(db_folder_path, key)) do
{:ok, contents} -> :erlang.binary_to_term(contents)
_ -> nil
end
{:reply, data, db_folder_path}
end
defp file_name(db_folder_path, key), do: "#{db_folder_path}/#{key}"
end
defmodule Todo.List do
defstruct auto_id: 1, entries: %{}
def new(entries \\ []) do
Enum.reduce(entries, %Todo.List{}, &add_entry(&2, &1))
end
def size(todo_list) do
Map.size(todo_list.entries)
end
def add_entry(%Todo.List{entries: entries, auto_id: auto_id} = todo_list, entry) do
entry = Map.put(entry, :id, auto_id)
new_entries = Map.put(entries, auto_id, entry)
%Todo.List{todo_list | entries: new_entries, auto_id: auto_id + 1 }
end
def entries(%Todo.List{entries: entries}, date) do
entries
|> Stream.filter(fn({_, entry}) -> entry.date == date end)
|> Enum.map(fn({_, entry}) -> entry end)
end
def update_entry(todo_list, %{} = new_entry) do
update_entry(todo_list, new_entry.id, fn(_) -> new_entry end)
end
def update_entry( %Todo.List{entries: entries} = todo_list, entry_id, updater_fun) do
case entries[entry_id] do
nil -> todo_list
old_entry ->
new_entry = updater_fun.(old_entry)
new_entries = Map.put(entries, new_entry.id, new_entry)
%Todo.List{todo_list | entries: new_entries}
end
end
def delete_entry( %Todo.List{entries: entries} = todo_list, entry_id) do
%Todo.List{todo_list | entries: Map.delete(entries, entry_id)}
end
end
defmodule Todo.Server do
use GenServer
# Client API
def start(name) do
GenServer.start(Todo.Server, name)
end
def add_entry(todo_server, new_entry) do
GenServer.cast(todo_server, {:add_entry, new_entry})
end
def entries(todo_server, date) do
GenServer.call(todo_server, {:entries, date})
end
# Server Callbacks
def init(name) do
send(self(), {:init, name})
{:ok, nil}
end
def handle_cast({:add_entry, new_entry}, {name, todo_list}) do
new_state = Todo.List.add_entry(todo_list, new_entry)
Todo.Database.store(name, new_state)
{:noreply, {name, new_state}}
end
def handle_call({:entries, date}, _from, {name, todo_list}) do
{:reply, Todo.List.entries(todo_list, date), {name, todo_list}}
end
# Needed for testing purposes
def handle_info({:init, name}, _state) do
{:noreply, {name, Todo.Database.get(name) || Todo.List.new()}}
end
def handle_info(:stop, state), do: {:stop, :normal, state}
def handle_info(_, state), do: {:noreply, state}
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment