├── .tool-versions ├── README.md └── code_samples ├── ch02 ├── arity_calc.ex ├── arity_demo.ex ├── geometry.ex ├── private_fun.ex ├── script.exs └── test │ └── tests.exs ├── ch03 ├── enum_streams_practice.ex ├── geometry.ex ├── geometry_invalid_input.ex ├── natural_nums.ex ├── rect.ex ├── recursion_practice.ex ├── recursion_practice_tc.ex ├── sum_list.ex ├── sum_list_tc.ex ├── test │ ├── test_file │ └── tests.exs ├── test_num.ex ├── test_num2.ex ├── user_extraction.ex └── user_extraction_2.ex ├── ch04 ├── fraction.ex ├── simple_todo.ex ├── test │ └── tests.exs ├── todo_builder.ex ├── todo_crud.ex ├── todo_entry_map.ex ├── todo_import.ex ├── todo_multi_dict.ex └── todos.csv ├── ch05 ├── calculator.ex ├── database_server.ex ├── process_bottleneck.ex ├── registered_todo_server.ex ├── stateful_database_server.ex ├── test │ └── tests.exs └── todo_server.ex ├── ch06 ├── key_value_gen_server.ex ├── server_process.ex ├── server_process_cast.ex ├── server_process_todo.ex ├── test │ └── tests.exs └── todo_server.ex ├── ch07 ├── persistable_todo_cache │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ │ └── todo │ │ │ ├── cache.ex │ │ │ ├── database.ex │ │ │ ├── list.ex │ │ │ └── server.ex │ ├── mix.exs │ ├── mix.lock │ └── test │ │ ├── test_helper.exs │ │ └── todo │ │ ├── cache_test.exs │ │ └── list_test.exs ├── todo │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ │ └── todo │ │ │ ├── list.ex │ │ │ └── server.ex │ ├── mix.exs │ ├── mix.lock │ └── test │ │ ├── test_helper.exs │ │ └── todo │ │ ├── list_test.exs │ │ └── server_test.exs ├── todo_cache │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ │ ├── load_test.ex │ │ └── todo │ │ │ ├── cache.ex │ │ │ ├── list.ex │ │ │ └── server.ex │ ├── mix.exs │ ├── mix.lock │ └── test │ │ ├── test_helper.exs │ │ └── todo │ │ ├── cache_test.exs │ │ └── list_test.exs └── todo_cache_pooling │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ └── todo │ │ ├── cache.ex │ │ ├── database.ex │ │ ├── database_worker.ex │ │ ├── list.ex │ │ └── server.ex │ ├── mix.exs │ ├── mix.lock │ └── test │ ├── test_helper.exs │ └── todo │ ├── cache_test.exs │ └── list_test.exs ├── ch08 ├── supervised_todo_cache │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ │ └── todo │ │ │ ├── cache.ex │ │ │ ├── database.ex │ │ │ ├── database_worker.ex │ │ │ ├── list.ex │ │ │ ├── server.ex │ │ │ └── system.ex │ ├── mix.exs │ ├── mix.lock │ └── test │ │ ├── test_helper.exs │ │ └── todo │ │ ├── cache_test.exs │ │ └── list_test.exs └── todo_links │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ └── todo │ │ ├── cache.ex │ │ ├── database.ex │ │ ├── database_worker.ex │ │ ├── list.ex │ │ ├── server.ex │ │ └── system.ex │ ├── mix.exs │ ├── mix.lock │ └── test │ ├── test_helper.exs │ └── todo │ ├── cache_test.exs │ └── list_test.exs ├── ch09 ├── dynamic_workers │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ │ └── todo │ │ │ ├── cache.ex │ │ │ ├── database.ex │ │ │ ├── database_worker.ex │ │ │ ├── list.ex │ │ │ ├── process_registry.ex │ │ │ ├── server.ex │ │ │ └── system.ex │ ├── mix.exs │ ├── mix.lock │ └── test │ │ ├── test_helper.exs │ │ └── todo │ │ ├── cache_test.exs │ │ └── list_test.exs ├── pool_supervision │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ │ └── todo │ │ │ ├── cache.ex │ │ │ ├── database.ex │ │ │ ├── database_worker.ex │ │ │ ├── list.ex │ │ │ ├── process_registry.ex │ │ │ ├── server.ex │ │ │ └── system.ex │ ├── mix.exs │ ├── mix.lock │ └── test │ │ ├── test_helper.exs │ │ └── todo │ │ ├── cache_test.exs │ │ └── list_test.exs └── supervise_database │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ └── todo │ │ ├── cache.ex │ │ ├── database.ex │ │ ├── database_worker.ex │ │ ├── list.ex │ │ ├── server.ex │ │ └── system.ex │ ├── mix.exs │ ├── mix.lock │ └── test │ ├── test_helper.exs │ └── todo │ ├── cache_test.exs │ └── list_test.exs ├── ch10 ├── key_value │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ │ ├── bench.ex │ │ ├── ets_key_value.ex │ │ ├── key_value.ex │ │ └── web_server.ex │ ├── mix.exs │ └── test │ │ ├── key_value_test.exs │ │ └── test_helper.exs ├── process_registry │ ├── ets.ex │ ├── gen_server.ex │ └── test │ │ └── tests.exs ├── todo_agent │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ │ └── todo │ │ │ ├── cache.ex │ │ │ ├── database.ex │ │ │ ├── database_worker.ex │ │ │ ├── list.ex │ │ │ ├── metrics.ex │ │ │ ├── process_registry.ex │ │ │ ├── server.ex │ │ │ └── system.ex │ ├── mix.exs │ ├── mix.lock │ └── test │ │ ├── test_helper.exs │ │ └── todo │ │ ├── cache_test.exs │ │ └── list_test.exs ├── todo_cache_expiry │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ │ └── todo │ │ │ ├── cache.ex │ │ │ ├── database.ex │ │ │ ├── database_worker.ex │ │ │ ├── list.ex │ │ │ ├── metrics.ex │ │ │ ├── process_registry.ex │ │ │ ├── server.ex │ │ │ └── system.ex │ ├── mix.exs │ ├── mix.lock │ └── test │ │ ├── test_helper.exs │ │ └── todo │ │ ├── cache_test.exs │ │ └── list_test.exs └── todo_metrics │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ └── todo │ │ ├── cache.ex │ │ ├── database.ex │ │ ├── database_worker.ex │ │ ├── list.ex │ │ ├── metrics.ex │ │ ├── process_registry.ex │ │ ├── server.ex │ │ └── system.ex │ ├── mix.exs │ ├── mix.lock │ └── test │ ├── test_helper.exs │ └── todo │ ├── cache_test.exs │ └── list_test.exs ├── ch11 ├── todo_app │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ │ └── todo │ │ │ ├── application.ex │ │ │ ├── cache.ex │ │ │ ├── database.ex │ │ │ ├── database_worker.ex │ │ │ ├── list.ex │ │ │ ├── metrics.ex │ │ │ ├── process_registry.ex │ │ │ ├── server.ex │ │ │ └── system.ex │ ├── mix.exs │ ├── mix.lock │ └── test │ │ ├── test_helper.exs │ │ └── todo │ │ ├── cache_test.exs │ │ └── list_test.exs ├── todo_env │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── config │ │ └── runtime.exs │ ├── lib │ │ └── todo │ │ │ ├── application.ex │ │ │ ├── cache.ex │ │ │ ├── database.ex │ │ │ ├── database_worker.ex │ │ │ ├── list.ex │ │ │ ├── metrics.ex │ │ │ ├── process_registry.ex │ │ │ ├── server.ex │ │ │ ├── system.ex │ │ │ └── web.ex │ ├── mix.exs │ ├── mix.lock │ ├── test │ │ ├── http_server_test.exs │ │ ├── test_helper.exs │ │ └── todo │ │ │ ├── cache_test.exs │ │ │ └── list_test.exs │ └── wrk.lua ├── todo_poolboy │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ │ └── todo │ │ │ ├── application.ex │ │ │ ├── cache.ex │ │ │ ├── database.ex │ │ │ ├── database_worker.ex │ │ │ ├── list.ex │ │ │ ├── metrics.ex │ │ │ ├── process_registry.ex │ │ │ ├── server.ex │ │ │ └── system.ex │ ├── mix.exs │ ├── mix.lock │ └── test │ │ ├── test_helper.exs │ │ └── todo │ │ ├── cache_test.exs │ │ └── list_test.exs └── todo_web │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ └── todo │ │ ├── application.ex │ │ ├── cache.ex │ │ ├── database.ex │ │ ├── database_worker.ex │ │ ├── list.ex │ │ ├── metrics.ex │ │ ├── process_registry.ex │ │ ├── server.ex │ │ ├── system.ex │ │ └── web.ex │ ├── mix.exs │ ├── mix.lock │ ├── test │ ├── http_server_test.exs │ ├── test_helper.exs │ └── todo │ │ ├── cache_test.exs │ │ └── list_test.exs │ └── wrk.lua ├── ch12 └── todo_distributed │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── config │ └── runtime.exs │ ├── lib │ └── todo │ │ ├── application.ex │ │ ├── cache.ex │ │ ├── database.ex │ │ ├── database_worker.ex │ │ ├── list.ex │ │ ├── metrics.ex │ │ ├── server.ex │ │ ├── system.ex │ │ └── web.ex │ ├── mix.exs │ ├── mix.lock │ ├── test │ ├── http_server_test.exs │ ├── test_helper.exs │ └── todo │ │ ├── cache_test.exs │ │ └── list_test.exs │ └── wrk.lua ├── ch13 └── todo_release │ ├── .formatter.exs │ ├── .gitignore │ ├── Dockerfile │ ├── README.md │ ├── config │ └── runtime.exs │ ├── lib │ └── todo │ │ ├── application.ex │ │ ├── cache.ex │ │ ├── database.ex │ │ ├── database_worker.ex │ │ ├── list.ex │ │ ├── metrics.ex │ │ ├── server.ex │ │ ├── system.ex │ │ └── web.ex │ ├── mix.exs │ ├── mix.lock │ ├── test │ ├── http_server_test.exs │ ├── test_helper.exs │ └── todo │ │ ├── cache_test.exs │ │ └── list_test.exs │ └── wrk.lua ├── test_all.exs └── test_helper.exs /.tool-versions: -------------------------------------------------------------------------------- 1 | erlang 26.0 2 | elixir 1.15.0-otp-26 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This branch contains the accompanying code for Elixir in Action, Third Edition. 2 | 3 | To compile and run the code, you'll need Elixir 1.15 and Erlang 26.x. If you're using the [asdf version manager](https://github.com/asdf-vm/asdf), keep in mind that this repository contains the `.tool-versions` file, specifying the Erlang and Elixir versions used to write this code. In this case you'll need to run `asdf install` in the root folder of this repository, to make sure the correct versions are installed. If you prefer to use different Elixir/Erlang versions, you can specify them in the `.tool-versions` file. 4 | 5 | A `git` client should also be installed and available somewhere in the execution path. 6 | 7 | The code for the second edition can be found [here](https://github.com/sasa1977/elixir-in-action/tree/2nd-edition). 8 | 9 | The code for the first edition can be found [here](https://github.com/sasa1977/elixir-in-action/tree/1st-edition). 10 | -------------------------------------------------------------------------------- /code_samples/ch02/arity_calc.ex: -------------------------------------------------------------------------------- 1 | defmodule Calculator do 2 | def add(a), do: add(a, 0) 3 | def add(a, b), do: a + b 4 | end 5 | -------------------------------------------------------------------------------- /code_samples/ch02/arity_demo.ex: -------------------------------------------------------------------------------- 1 | defmodule Rectangle do 2 | def area(a), do: area(a, a) 3 | def area(a, b), do: a * b 4 | end 5 | -------------------------------------------------------------------------------- /code_samples/ch02/geometry.ex: -------------------------------------------------------------------------------- 1 | defmodule Geometry do 2 | def rectangle_area(a, b) do 3 | a * b 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /code_samples/ch02/private_fun.ex: -------------------------------------------------------------------------------- 1 | defmodule TestPrivate do 2 | def double(a) do 3 | sum(a, a) 4 | end 5 | 6 | defp sum(a, b) do 7 | a + b 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /code_samples/ch02/script.exs: -------------------------------------------------------------------------------- 1 | defmodule MyModule do 2 | def run do 3 | IO.puts("Called MyModule.run") 4 | end 5 | end 6 | 7 | MyModule.run() 8 | -------------------------------------------------------------------------------- /code_samples/ch02/test/tests.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("#{__DIR__}/../../test_helper.exs") 2 | 3 | defmodule Test do 4 | use ExUnit.Case, async: false 5 | import TestHelper 6 | 7 | test_script "private_fun" do 8 | assert 4 == TestPrivate.double(2) 9 | end 10 | 11 | test_script "arity_calc" do 12 | assert 1 == Calculator.add(1) 13 | assert 3 == Calculator.add(1, 2) 14 | end 15 | 16 | test_script "arity_demo" do 17 | assert 4 == Rectangle.area(2) 18 | assert 6 == Rectangle.area(2, 3) 19 | end 20 | 21 | test_script "geometry" do 22 | assert 6 == Geometry.rectangle_area(3, 2) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /code_samples/ch03/enum_streams_practice.ex: -------------------------------------------------------------------------------- 1 | defmodule EnumStreams do 2 | defp filtered_lines!(path) do 3 | path 4 | |> File.stream!() 5 | |> Stream.map(&String.trim_trailing(&1, "\n")) 6 | end 7 | 8 | def lines_lengths!(path) do 9 | path 10 | |> filtered_lines!() 11 | |> Enum.map(&String.length/1) 12 | end 13 | 14 | def longest_line_length!(path) do 15 | path 16 | |> filtered_lines!() 17 | |> Stream.map(&String.length/1) 18 | |> Enum.max() 19 | end 20 | 21 | def longest_line!(path) do 22 | path 23 | |> filtered_lines!() 24 | |> Enum.max_by(&String.length/1) 25 | end 26 | 27 | def words_per_line!(path) do 28 | path 29 | |> filtered_lines!() 30 | |> Enum.map(&word_count/1) 31 | end 32 | 33 | defp word_count(string) do 34 | string 35 | |> String.split() 36 | |> length() 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /code_samples/ch03/geometry.ex: -------------------------------------------------------------------------------- 1 | defmodule Geometry do 2 | def area({:rectangle, a, b}) do 3 | a * b 4 | end 5 | 6 | def area({:square, a}) do 7 | a * a 8 | end 9 | 10 | def area({:circle, r}) do 11 | r * r * 3.14 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /code_samples/ch03/geometry_invalid_input.ex: -------------------------------------------------------------------------------- 1 | defmodule Geometry do 2 | def area({:rectangle, a, b}) do 3 | a * b 4 | end 5 | 6 | def area({:square, a}) do 7 | a * a 8 | end 9 | 10 | def area({:circle, r}) do 11 | r * r * 3.14 12 | end 13 | 14 | def area(unknown) do 15 | {:error, {:unknown_shape, unknown}} 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /code_samples/ch03/natural_nums.ex: -------------------------------------------------------------------------------- 1 | defmodule NaturalNums do 2 | def print(1), do: IO.puts(1) 3 | 4 | def print(n) do 5 | print(n - 1) 6 | IO.puts(n) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /code_samples/ch03/rect.ex: -------------------------------------------------------------------------------- 1 | defmodule Rectangle do 2 | def area({a, b}) do 3 | a * b 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /code_samples/ch03/recursion_practice.ex: -------------------------------------------------------------------------------- 1 | defmodule Loop do 2 | def list_len([]), do: 0 3 | 4 | def list_len([_ | tail]) do 5 | 1 + list_len(tail) 6 | end 7 | 8 | def range(from, to) when from > to do 9 | [] 10 | end 11 | 12 | def range(from, to) do 13 | [from | range(from + 1, to)] 14 | end 15 | 16 | def positive([]), do: [] 17 | 18 | def positive([head | tail]) when head > 0 do 19 | [head | positive(tail)] 20 | end 21 | 22 | def positive([_ | tail]) do 23 | positive(tail) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /code_samples/ch03/sum_list.ex: -------------------------------------------------------------------------------- 1 | defmodule ListHelper do 2 | def sum([]), do: 0 3 | 4 | def sum([head | tail]) do 5 | head + sum(tail) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /code_samples/ch03/sum_list_tc.ex: -------------------------------------------------------------------------------- 1 | defmodule ListHelper do 2 | def sum(list) do 3 | do_sum(0, list) 4 | end 5 | 6 | defp do_sum(current_sum, []) do 7 | current_sum 8 | end 9 | 10 | defp do_sum(current_sum, [head | tail]) do 11 | new_sum = head + current_sum 12 | do_sum(new_sum, tail) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /code_samples/ch03/test/test_file: -------------------------------------------------------------------------------- 1 | foo 2 | foo bar 3 | foo bar baz -------------------------------------------------------------------------------- /code_samples/ch03/test_num.ex: -------------------------------------------------------------------------------- 1 | defmodule TestNum do 2 | def test(x) when x < 0 do 3 | :negative 4 | end 5 | 6 | def test(x) when x == 0 do 7 | :zero 8 | end 9 | 10 | def test(x) when x > 0 do 11 | :positive 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /code_samples/ch03/test_num2.ex: -------------------------------------------------------------------------------- 1 | defmodule TestNum do 2 | def test(x) when is_number(x) and x < 0 do 3 | :negative 4 | end 5 | 6 | def test(x) when x == 0 do 7 | :zero 8 | end 9 | 10 | def test(x) when is_number(x) and x > 0 do 11 | :positive 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /code_samples/ch03/user_extraction.ex: -------------------------------------------------------------------------------- 1 | defmodule UserExtraction do 2 | def extract_user(user) do 3 | with {:ok, login} <- extract_login(user), 4 | {:ok, email} <- extract_email(user), 5 | {:ok, password} <- extract_password(user) do 6 | {:ok, %{login: login, email: email, password: password}} 7 | end 8 | end 9 | 10 | defp extract_login(%{"login" => login}), do: {:ok, login} 11 | defp extract_login(_), do: {:error, "login missing"} 12 | 13 | defp extract_email(%{"email" => email}), do: {:ok, email} 14 | defp extract_email(_), do: {:error, "email missing"} 15 | 16 | defp extract_password(%{"password" => password}), do: {:ok, password} 17 | defp extract_password(_), do: {:error, "password missing"} 18 | end 19 | -------------------------------------------------------------------------------- /code_samples/ch03/user_extraction_2.ex: -------------------------------------------------------------------------------- 1 | defmodule UserExtraction do 2 | def extract_user(user) do 3 | case Enum.filter( 4 | ["login", "email", "password"], 5 | &(not Map.has_key?(user, &1)) 6 | ) do 7 | [] -> 8 | {:ok, %{ 9 | login: user["login"], 10 | email: user["email"], 11 | password: user["password"] 12 | }} 13 | 14 | missing_fields -> 15 | {:error, "missing fields: #{Enum.join(missing_fields, ", ")}"} 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /code_samples/ch04/fraction.ex: -------------------------------------------------------------------------------- 1 | defmodule Fraction do 2 | defstruct a: nil, b: nil 3 | 4 | def new(a, b) do 5 | %Fraction{a: a, b: b} 6 | end 7 | 8 | def value(%Fraction{a: a, b: b}) do 9 | a / b 10 | end 11 | 12 | def add(%Fraction{a: a1, b: b1}, %Fraction{a: a2, b: b2}) do 13 | new( 14 | a1 * b2 + a2 * b1, 15 | b2 * b1 16 | ) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /code_samples/ch04/simple_todo.ex: -------------------------------------------------------------------------------- 1 | defmodule TodoList do 2 | def new(), do: %{} 3 | 4 | def add_entry(todo_list, date, title) do 5 | Map.update(todo_list, date, [title], fn titles -> [title | titles] end) 6 | end 7 | 8 | def entries(todo_list, date) do 9 | Map.get(todo_list, date, []) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /code_samples/ch04/todo_builder.ex: -------------------------------------------------------------------------------- 1 | defmodule TodoList do 2 | defstruct next_id: 1, entries: %{} 3 | 4 | def new(entries \\ []) do 5 | Enum.reduce( 6 | entries, 7 | %TodoList{}, 8 | &add_entry(&2, &1) 9 | ) 10 | end 11 | 12 | def add_entry(todo_list, entry) do 13 | entry = Map.put(entry, :id, todo_list.next_id) 14 | new_entries = Map.put(todo_list.entries, todo_list.next_id, entry) 15 | 16 | %TodoList{todo_list | entries: new_entries, next_id: todo_list.next_id + 1} 17 | end 18 | 19 | def entries(todo_list, date) do 20 | todo_list.entries 21 | |> Map.values() 22 | |> Enum.filter(fn entry -> entry.date == date end) 23 | end 24 | 25 | def update_entry(todo_list, entry_id, updater_fun) do 26 | case Map.fetch(todo_list.entries, entry_id) do 27 | :error -> 28 | todo_list 29 | 30 | {:ok, old_entry} -> 31 | new_entry = updater_fun.(old_entry) 32 | new_entries = Map.put(todo_list.entries, new_entry.id, new_entry) 33 | %TodoList{todo_list | entries: new_entries} 34 | end 35 | end 36 | 37 | def delete_entry(todo_list, entry_id) do 38 | %TodoList{todo_list | entries: Map.delete(todo_list.entries, entry_id)} 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /code_samples/ch04/todo_crud.ex: -------------------------------------------------------------------------------- 1 | defmodule TodoList do 2 | defstruct next_id: 1, entries: %{} 3 | 4 | def new(), do: %TodoList{} 5 | 6 | def add_entry(todo_list, entry) do 7 | entry = Map.put(entry, :id, todo_list.next_id) 8 | new_entries = Map.put(todo_list.entries, todo_list.next_id, entry) 9 | 10 | %TodoList{todo_list | entries: new_entries, next_id: todo_list.next_id + 1} 11 | end 12 | 13 | def entries(todo_list, date) do 14 | todo_list.entries 15 | |> Map.values() 16 | |> Enum.filter(fn entry -> entry.date == date end) 17 | end 18 | 19 | def update_entry(todo_list, entry_id, updater_fun) do 20 | case Map.fetch(todo_list.entries, entry_id) do 21 | :error -> 22 | todo_list 23 | 24 | {:ok, old_entry} -> 25 | new_entry = updater_fun.(old_entry) 26 | new_entries = Map.put(todo_list.entries, new_entry.id, new_entry) 27 | %TodoList{todo_list | entries: new_entries} 28 | end 29 | end 30 | 31 | def delete_entry(todo_list, entry_id) do 32 | %TodoList{todo_list | entries: Map.delete(todo_list.entries, entry_id)} 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /code_samples/ch04/todo_entry_map.ex: -------------------------------------------------------------------------------- 1 | defmodule MultiDict do 2 | def new(), do: %{} 3 | 4 | def add(dict, key, value) do 5 | Map.update( 6 | dict, 7 | key, 8 | [value], 9 | &[value | &1] 10 | ) 11 | end 12 | 13 | def get(dict, key) do 14 | Map.get(dict, key, []) 15 | end 16 | end 17 | 18 | defmodule TodoList do 19 | def new(), do: MultiDict.new() 20 | 21 | def add_entry(todo_list, entry) do 22 | MultiDict.add(todo_list, entry.date, entry) 23 | end 24 | 25 | def entries(todo_list, date) do 26 | MultiDict.get(todo_list, date) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /code_samples/ch04/todo_multi_dict.ex: -------------------------------------------------------------------------------- 1 | defmodule MultiDict do 2 | def new(), do: %{} 3 | 4 | def add(dict, key, value) do 5 | Map.update(dict, key, [value], &[value | &1]) 6 | end 7 | 8 | def get(dict, key) do 9 | Map.get(dict, key, []) 10 | end 11 | end 12 | 13 | defmodule TodoList do 14 | def new(), do: MultiDict.new() 15 | 16 | def add_entry(todo_list, date, title) do 17 | MultiDict.add(todo_list, date, title) 18 | end 19 | 20 | def entries(todo_list, date) do 21 | MultiDict.get(todo_list, date) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /code_samples/ch04/todos.csv: -------------------------------------------------------------------------------- 1 | 2018-12-19,Dentist 2 | 2018-12-20,Shopping 3 | 2018-12-19,Movies 4 | -------------------------------------------------------------------------------- /code_samples/ch05/calculator.ex: -------------------------------------------------------------------------------- 1 | defmodule Calculator do 2 | def start do 3 | spawn(fn -> loop(0) end) 4 | end 5 | 6 | def value(server_pid) do 7 | send(server_pid, {:value, self()}) 8 | 9 | receive do 10 | {:response, value} -> 11 | value 12 | end 13 | end 14 | 15 | def add(server_pid, value), do: send(server_pid, {:add, value}) 16 | def sub(server_pid, value), do: send(server_pid, {:sub, value}) 17 | def mul(server_pid, value), do: send(server_pid, {:mul, value}) 18 | def div(server_pid, value), do: send(server_pid, {:div, value}) 19 | 20 | defp loop(current_value) do 21 | new_value = 22 | receive do 23 | {:value, caller} -> 24 | send(caller, {:response, current_value}) 25 | current_value 26 | 27 | {:add, value} -> 28 | current_value + value 29 | 30 | {:sub, value} -> 31 | current_value - value 32 | 33 | {:mul, value} -> 34 | current_value * value 35 | 36 | {:div, value} -> 37 | current_value / value 38 | 39 | invalid_request -> 40 | IO.puts("invalid request #{inspect(invalid_request)}") 41 | end 42 | 43 | loop(new_value) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /code_samples/ch05/database_server.ex: -------------------------------------------------------------------------------- 1 | defmodule DatabaseServer do 2 | def start do 3 | spawn(&loop/0) 4 | end 5 | 6 | def run_async(server_pid, query_def) do 7 | send(server_pid, {:run_query, self(), query_def}) 8 | end 9 | 10 | def get_result do 11 | receive do 12 | {:query_result, result} -> result 13 | after 14 | 5000 -> {:error, :timeout} 15 | end 16 | end 17 | 18 | defp loop do 19 | receive do 20 | {:run_query, caller, query_def} -> 21 | query_result = run_query(query_def) 22 | send(caller, {:query_result, query_result}) 23 | end 24 | 25 | loop() 26 | end 27 | 28 | defp run_query(query_def) do 29 | Process.sleep(2000) 30 | "#{query_def} result" 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /code_samples/ch05/process_bottleneck.ex: -------------------------------------------------------------------------------- 1 | defmodule Server do 2 | def start do 3 | spawn(fn -> loop() end) 4 | end 5 | 6 | def send_msg(server, message) do 7 | send(server, {self(), message}) 8 | 9 | receive do 10 | {:response, response} -> response 11 | end 12 | end 13 | 14 | defp loop do 15 | receive do 16 | {caller, msg} -> 17 | Process.sleep(1000) 18 | send(caller, {:response, msg}) 19 | end 20 | 21 | loop() 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /code_samples/ch05/stateful_database_server.ex: -------------------------------------------------------------------------------- 1 | defmodule DatabaseServer do 2 | def start do 3 | spawn(fn -> 4 | connection = :rand.uniform(1000) 5 | loop(connection) 6 | end) 7 | end 8 | 9 | def run_async(server_pid, query_def) do 10 | send(server_pid, {:run_query, self(), query_def}) 11 | end 12 | 13 | def get_result do 14 | receive do 15 | {:query_result, result} -> result 16 | after 17 | 5000 -> {:error, :timeout} 18 | end 19 | end 20 | 21 | defp loop(connection) do 22 | receive do 23 | {:run_query, caller, query_def} -> 24 | query_result = run_query(connection, query_def) 25 | send(caller, {:query_result, query_result}) 26 | end 27 | 28 | loop(connection) 29 | end 30 | 31 | defp run_query(connection, query_def) do 32 | Process.sleep(2000) 33 | "Connection #{connection}: #{query_def} result" 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /code_samples/ch06/key_value_gen_server.ex: -------------------------------------------------------------------------------- 1 | defmodule KeyValueStore do 2 | use GenServer 3 | 4 | def start do 5 | GenServer.start(KeyValueStore, nil) 6 | end 7 | 8 | def put(pid, key, value) do 9 | GenServer.cast(pid, {:put, key, value}) 10 | end 11 | 12 | def get(pid, key) do 13 | GenServer.call(pid, {:get, key}) 14 | end 15 | 16 | def init(_) do 17 | {:ok, %{}} 18 | end 19 | 20 | def handle_cast({:put, key, value}, state) do 21 | {:noreply, Map.put(state, key, value)} 22 | end 23 | 24 | def handle_call({:get, key}, _, state) do 25 | {:reply, Map.get(state, key), state} 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /code_samples/ch06/server_process.ex: -------------------------------------------------------------------------------- 1 | defmodule ServerProcess do 2 | def start(callback_module) do 3 | spawn(fn -> 4 | initial_state = callback_module.init() 5 | loop(callback_module, initial_state) 6 | end) 7 | end 8 | 9 | defp loop(callback_module, current_state) do 10 | receive do 11 | {request, caller} -> 12 | {response, new_state} = 13 | callback_module.handle_call( 14 | request, 15 | current_state 16 | ) 17 | 18 | send(caller, {:response, response}) 19 | loop(callback_module, new_state) 20 | end 21 | end 22 | 23 | def call(server_pid, request) do 24 | send(server_pid, {request, self()}) 25 | 26 | receive do 27 | {:response, response} -> 28 | response 29 | end 30 | end 31 | end 32 | 33 | defmodule KeyValueStore do 34 | def start do 35 | ServerProcess.start(KeyValueStore) 36 | end 37 | 38 | def put(pid, key, value) do 39 | ServerProcess.call(pid, {:put, key, value}) 40 | end 41 | 42 | def get(pid, key) do 43 | ServerProcess.call(pid, {:get, key}) 44 | end 45 | 46 | def init do 47 | %{} 48 | end 49 | 50 | def handle_call({:put, key, value}, state) do 51 | {:ok, Map.put(state, key, value)} 52 | end 53 | 54 | def handle_call({:get, key}, state) do 55 | {Map.get(state, key), state} 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /code_samples/ch07/persistable_todo_cache/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /code_samples/ch07/persistable_todo_cache/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | /persist/ 6 | -------------------------------------------------------------------------------- /code_samples/ch07/persistable_todo_cache/README.md: -------------------------------------------------------------------------------- 1 | This is a demo project for Elixir in Action. 2 | 3 | To be able to compile and run it, you'll need Elixir 1.0.0 and Erlang 17.0. A `git` client should also be installed and available somewhere in the execution path. 4 | 5 | Prior to the first build, you need to call `mix deps.get`. After that, you can build and start the interactive shell by running `iex -S mix` from this project root. -------------------------------------------------------------------------------- /code_samples/ch07/persistable_todo_cache/lib/todo/cache.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Cache do 2 | use GenServer 3 | 4 | def start do 5 | GenServer.start(__MODULE__, nil) 6 | end 7 | 8 | def server_process(cache_pid, todo_list_name) do 9 | GenServer.call(cache_pid, {:server_process, todo_list_name}) 10 | end 11 | 12 | @impl GenServer 13 | def init(_) do 14 | Todo.Database.start() 15 | {:ok, %{}} 16 | end 17 | 18 | @impl GenServer 19 | def handle_call({:server_process, todo_list_name}, _, todo_servers) do 20 | case Map.fetch(todo_servers, todo_list_name) do 21 | {:ok, todo_server} -> 22 | {:reply, todo_server, todo_servers} 23 | 24 | :error -> 25 | {:ok, new_server} = Todo.Server.start(todo_list_name) 26 | 27 | { 28 | :reply, 29 | new_server, 30 | Map.put(todo_servers, todo_list_name, new_server) 31 | } 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /code_samples/ch07/persistable_todo_cache/lib/todo/database.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Database do 2 | use GenServer 3 | 4 | @db_folder "./persist" 5 | 6 | def start do 7 | GenServer.start(__MODULE__, nil, name: __MODULE__) 8 | end 9 | 10 | def store(key, data) do 11 | GenServer.cast(__MODULE__, {:store, key, data}) 12 | end 13 | 14 | def get(key) do 15 | GenServer.call(__MODULE__, {:get, key}) 16 | end 17 | 18 | @impl GenServer 19 | def init(_) do 20 | File.mkdir_p!(@db_folder) 21 | {:ok, nil} 22 | end 23 | 24 | @impl GenServer 25 | def handle_cast({:store, key, data}, state) do 26 | key 27 | |> file_name() 28 | |> File.write!(:erlang.term_to_binary(data)) 29 | 30 | {:noreply, state} 31 | end 32 | 33 | @impl GenServer 34 | def handle_call({:get, key}, _, state) do 35 | data = 36 | case File.read(file_name(key)) do 37 | {:ok, contents} -> :erlang.binary_to_term(contents) 38 | _ -> nil 39 | end 40 | 41 | {:reply, data, state} 42 | end 43 | 44 | defp file_name(key) do 45 | Path.join(@db_folder, to_string(key)) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /code_samples/ch07/persistable_todo_cache/lib/todo/list.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.List do 2 | defstruct next_id: 1, entries: %{} 3 | 4 | def new(entries \\ []) do 5 | Enum.reduce( 6 | entries, 7 | %Todo.List{}, 8 | &add_entry(&2, &1) 9 | ) 10 | end 11 | 12 | def size(todo_list) do 13 | map_size(todo_list.entries) 14 | end 15 | 16 | def add_entry(todo_list, entry) do 17 | entry = Map.put(entry, :id, todo_list.next_id) 18 | new_entries = Map.put(todo_list.entries, todo_list.next_id, entry) 19 | 20 | %Todo.List{todo_list | entries: new_entries, next_id: todo_list.next_id + 1} 21 | end 22 | 23 | def entries(todo_list, date) do 24 | todo_list.entries 25 | |> Map.values() 26 | |> Enum.filter(fn entry -> entry.date == date end) 27 | end 28 | 29 | def update_entry(todo_list, entry_id, updater_fun) do 30 | case Map.fetch(todo_list.entries, entry_id) do 31 | :error -> 32 | todo_list 33 | 34 | {:ok, old_entry} -> 35 | new_entry = updater_fun.(old_entry) 36 | new_entries = Map.put(todo_list.entries, new_entry.id, new_entry) 37 | %Todo.List{todo_list | entries: new_entries} 38 | end 39 | end 40 | 41 | def delete_entry(todo_list, entry_id) do 42 | %Todo.List{todo_list | entries: Map.delete(todo_list.entries, entry_id)} 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /code_samples/ch07/persistable_todo_cache/lib/todo/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Server do 2 | use GenServer 3 | 4 | def start(name) do 5 | GenServer.start(Todo.Server, name) 6 | end 7 | 8 | def add_entry(todo_server, new_entry) do 9 | GenServer.cast(todo_server, {:add_entry, new_entry}) 10 | end 11 | 12 | def entries(todo_server, date) do 13 | GenServer.call(todo_server, {:entries, date}) 14 | end 15 | 16 | @impl GenServer 17 | def init(name) do 18 | {:ok, {name, nil}, {:continue, :init}} 19 | end 20 | 21 | @impl GenServer 22 | def handle_continue(:init, {name, nil}) do 23 | todo_list = Todo.Database.get(name) || Todo.List.new() 24 | {:noreply, {name, todo_list}} 25 | end 26 | 27 | @impl GenServer 28 | def handle_cast({:add_entry, new_entry}, {name, todo_list}) do 29 | new_list = Todo.List.add_entry(todo_list, new_entry) 30 | Todo.Database.store(name, new_list) 31 | {:noreply, {name, new_list}} 32 | end 33 | 34 | @impl GenServer 35 | def handle_call({:entries, date}, _, {name, todo_list}) do 36 | { 37 | :reply, 38 | Todo.List.entries(todo_list, date), 39 | {name, todo_list} 40 | } 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /code_samples/ch07/persistable_todo_cache/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :todo, 7 | version: "0.1.0", 8 | elixir: "~> 1.15", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | def application do 15 | [ 16 | extra_applications: [:logger] 17 | ] 18 | end 19 | 20 | defp deps do 21 | [] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /code_samples/ch07/persistable_todo_cache/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [:rebar3], [], "hexpm"}, 3 | } 4 | -------------------------------------------------------------------------------- /code_samples/ch07/persistable_todo_cache/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | File.rm_rf("./persist") 2 | File.mkdir_p("./persist") 3 | ExUnit.start() 4 | -------------------------------------------------------------------------------- /code_samples/ch07/persistable_todo_cache/test/todo/cache_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.CacheTest do 2 | use ExUnit.Case 3 | 4 | test "server_process" do 5 | {:ok, cache} = Todo.Cache.start() 6 | bob_pid = Todo.Cache.server_process(cache, "bob") 7 | 8 | assert bob_pid != Todo.Cache.server_process(cache, "alice") 9 | assert bob_pid == Todo.Cache.server_process(cache, "bob") 10 | end 11 | 12 | test "to-do operations" do 13 | {:ok, cache} = Todo.Cache.start() 14 | alice = Todo.Cache.server_process(cache, "alice") 15 | Todo.Server.add_entry(alice, %{date: ~D[2018-12-19], title: "Dentist"}) 16 | entries = Todo.Server.entries(alice, ~D[2018-12-19]) 17 | 18 | assert [%{date: ~D[2018-12-19], title: "Dentist"}] = entries 19 | end 20 | 21 | test "persistence" do 22 | {:ok, cache} = Todo.Cache.start() 23 | 24 | john = Todo.Cache.server_process(cache, "john") 25 | Todo.Server.add_entry(john, %{date: ~D[2018-12-20], title: "Shopping"}) 26 | assert 1 == length(Todo.Server.entries(john, ~D[2018-12-20])) 27 | 28 | GenServer.stop(cache) 29 | {:ok, cache} = Todo.Cache.start() 30 | 31 | entries = 32 | cache 33 | |> Todo.Cache.server_process("john") 34 | |> Todo.Server.entries(~D[2018-12-20]) 35 | 36 | assert [%{date: ~D[2018-12-20], title: "Shopping"}] = entries 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /code_samples/ch07/todo/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /code_samples/ch07/todo/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | -------------------------------------------------------------------------------- /code_samples/ch07/todo/README.md: -------------------------------------------------------------------------------- 1 | This is a demo project for Elixir in Action. 2 | 3 | To be able to compile and run it, you'll need Elixir 1.0.0 and Erlang 17.0. A `git` client should also be installed and available somewhere in the execution path. 4 | 5 | Prior to the first build, you need to call `mix deps.get`. After that, you can build and start the interactive shell by running `iex -S mix` from this project root. -------------------------------------------------------------------------------- /code_samples/ch07/todo/lib/todo/list.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.List do 2 | defstruct next_id: 1, entries: %{} 3 | 4 | def new(entries \\ []) do 5 | Enum.reduce( 6 | entries, 7 | %Todo.List{}, 8 | &add_entry(&2, &1) 9 | ) 10 | end 11 | 12 | def size(todo_list) do 13 | map_size(todo_list.entries) 14 | end 15 | 16 | def add_entry(todo_list, entry) do 17 | entry = Map.put(entry, :id, todo_list.next_id) 18 | new_entries = Map.put(todo_list.entries, todo_list.next_id, entry) 19 | 20 | %Todo.List{todo_list | entries: new_entries, next_id: todo_list.next_id + 1} 21 | end 22 | 23 | def entries(todo_list, date) do 24 | todo_list.entries 25 | |> Map.values() 26 | |> Enum.filter(fn entry -> entry.date == date end) 27 | end 28 | 29 | def update_entry(todo_list, entry_id, updater_fun) do 30 | case Map.fetch(todo_list.entries, entry_id) do 31 | :error -> 32 | todo_list 33 | 34 | {:ok, old_entry} -> 35 | new_entry = updater_fun.(old_entry) 36 | new_entries = Map.put(todo_list.entries, new_entry.id, new_entry) 37 | %Todo.List{todo_list | entries: new_entries} 38 | end 39 | end 40 | 41 | def delete_entry(todo_list, entry_id) do 42 | %Todo.List{todo_list | entries: Map.delete(todo_list.entries, entry_id)} 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /code_samples/ch07/todo/lib/todo/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Server do 2 | use GenServer 3 | 4 | def start do 5 | GenServer.start(__MODULE__, nil) 6 | end 7 | 8 | def add_entry(todo_server, new_entry) do 9 | GenServer.cast(todo_server, {:add_entry, new_entry}) 10 | end 11 | 12 | def entries(todo_server, date) do 13 | GenServer.call(todo_server, {:entries, date}) 14 | end 15 | 16 | @impl GenServer 17 | def init(_) do 18 | {:ok, Todo.List.new()} 19 | end 20 | 21 | @impl GenServer 22 | def handle_cast({:add_entry, new_entry}, todo_list) do 23 | new_state = Todo.List.add_entry(todo_list, new_entry) 24 | {:noreply, new_state} 25 | end 26 | 27 | @impl GenServer 28 | def handle_call({:entries, date}, _, todo_list) do 29 | { 30 | :reply, 31 | Todo.List.entries(todo_list, date), 32 | todo_list 33 | } 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /code_samples/ch07/todo/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :todo, 7 | version: "0.1.0", 8 | elixir: "~> 1.15", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | def application do 15 | [ 16 | extra_applications: [:logger] 17 | ] 18 | end 19 | 20 | defp deps do 21 | [] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /code_samples/ch07/todo/mix.lock: -------------------------------------------------------------------------------- 1 | %{} 2 | -------------------------------------------------------------------------------- /code_samples/ch07/todo/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /code_samples/ch07/todo/test/todo/server_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.ServerTest do 2 | use ExUnit.Case, async: true 3 | 4 | setup do 5 | {:ok, todo_server} = Todo.Server.start() 6 | on_exit(fn -> GenServer.stop(todo_server) end) 7 | {:ok, todo_server: todo_server} 8 | end 9 | 10 | test "add_entry", context do 11 | assert([] == Todo.Server.entries(context[:todo_server], ~D[2018-12-19])) 12 | 13 | Todo.Server.add_entry(context[:todo_server], %{date: ~D[2018-12-19], title: "Dentist"}) 14 | assert(1 == Todo.Server.entries(context[:todo_server], ~D[2018-12-19]) |> length) 15 | 16 | assert( 17 | "Dentist" == 18 | (Todo.Server.entries(context[:todo_server], ~D[2018-12-19]) |> Enum.at(0)).title 19 | ) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /code_samples/ch07/todo_cache/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /code_samples/ch07/todo_cache/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | -------------------------------------------------------------------------------- /code_samples/ch07/todo_cache/README.md: -------------------------------------------------------------------------------- 1 | This is a demo project for Elixir in Action. 2 | 3 | To be able to compile and run it, you'll need Elixir 1.0.0 and Erlang 17.0. A `git` client should also be installed and available somewhere in the execution path. 4 | 5 | Prior to the first build, you need to call `mix deps.get`. After that, you can build and start the interactive shell by running `iex -S mix` from this project root. -------------------------------------------------------------------------------- /code_samples/ch07/todo_cache/lib/load_test.ex: -------------------------------------------------------------------------------- 1 | # Very quick, inconclusive load test 2 | # 3 | # Start from command line with: 4 | # elixir --erl "+P 2000000" -S mix run -e LoadTest.run 5 | # 6 | # Note: the +P 2000000 sets maximum number of processes to 2 millions 7 | defmodule LoadTest do 8 | def run do 9 | {:ok, cache} = Todo.Cache.start() 10 | 11 | total_processes = 1_000_000 12 | 13 | # Since the cache is empty, this code creates new processes. 14 | {put_time, _} = 15 | :timer.tc(fn -> 16 | Enum.each( 17 | 1..total_processes, 18 | &Todo.Cache.server_process(cache, "cache_#{&1}") 19 | ) 20 | end) 21 | 22 | IO.puts("average put #{put_time / total_processes} μs") 23 | 24 | # Since the cache is primed, and we use the same names as in the 25 | # previous loop, this code benches process retrieval. 26 | {get_time, _} = 27 | :timer.tc(fn -> 28 | Enum.each( 29 | 1..total_processes, 30 | &Todo.Cache.server_process(cache, "cache_#{&1}") 31 | ) 32 | end) 33 | 34 | IO.puts("average get #{get_time / total_processes} μs") 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /code_samples/ch07/todo_cache/lib/todo/cache.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Cache do 2 | use GenServer 3 | 4 | def start do 5 | GenServer.start(__MODULE__, nil) 6 | end 7 | 8 | def server_process(cache_pid, todo_list_name) do 9 | GenServer.call(cache_pid, {:server_process, todo_list_name}) 10 | end 11 | 12 | @impl GenServer 13 | def init(_) do 14 | {:ok, %{}} 15 | end 16 | 17 | @impl GenServer 18 | def handle_call({:server_process, todo_list_name}, _, todo_servers) do 19 | case Map.fetch(todo_servers, todo_list_name) do 20 | {:ok, todo_server} -> 21 | {:reply, todo_server, todo_servers} 22 | 23 | :error -> 24 | {:ok, new_server} = Todo.Server.start() 25 | 26 | { 27 | :reply, 28 | new_server, 29 | Map.put(todo_servers, todo_list_name, new_server) 30 | } 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /code_samples/ch07/todo_cache/lib/todo/list.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.List do 2 | defstruct next_id: 1, entries: %{} 3 | 4 | def new(entries \\ []) do 5 | Enum.reduce( 6 | entries, 7 | %Todo.List{}, 8 | &add_entry(&2, &1) 9 | ) 10 | end 11 | 12 | def size(todo_list) do 13 | map_size(todo_list.entries) 14 | end 15 | 16 | def add_entry(todo_list, entry) do 17 | entry = Map.put(entry, :id, todo_list.next_id) 18 | new_entries = Map.put(todo_list.entries, todo_list.next_id, entry) 19 | 20 | %Todo.List{todo_list | entries: new_entries, next_id: todo_list.next_id + 1} 21 | end 22 | 23 | def entries(todo_list, date) do 24 | todo_list.entries 25 | |> Map.values() 26 | |> Enum.filter(fn entry -> entry.date == date end) 27 | end 28 | 29 | def update_entry(todo_list, entry_id, updater_fun) do 30 | case Map.fetch(todo_list.entries, entry_id) do 31 | :error -> 32 | todo_list 33 | 34 | {:ok, old_entry} -> 35 | new_entry = updater_fun.(old_entry) 36 | new_entries = Map.put(todo_list.entries, new_entry.id, new_entry) 37 | %Todo.List{todo_list | entries: new_entries} 38 | end 39 | end 40 | 41 | def delete_entry(todo_list, entry_id) do 42 | %Todo.List{todo_list | entries: Map.delete(todo_list.entries, entry_id)} 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /code_samples/ch07/todo_cache/lib/todo/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Server do 2 | use GenServer 3 | 4 | def start do 5 | GenServer.start(__MODULE__, nil) 6 | end 7 | 8 | def add_entry(todo_server, new_entry) do 9 | GenServer.cast(todo_server, {:add_entry, new_entry}) 10 | end 11 | 12 | def entries(todo_server, date) do 13 | GenServer.call(todo_server, {:entries, date}) 14 | end 15 | 16 | @impl GenServer 17 | def init(_) do 18 | {:ok, Todo.List.new()} 19 | end 20 | 21 | @impl GenServer 22 | def handle_cast({:add_entry, new_entry}, todo_list) do 23 | new_state = Todo.List.add_entry(todo_list, new_entry) 24 | {:noreply, new_state} 25 | end 26 | 27 | @impl GenServer 28 | def handle_call({:entries, date}, _, todo_list) do 29 | { 30 | :reply, 31 | Todo.List.entries(todo_list, date), 32 | todo_list 33 | } 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /code_samples/ch07/todo_cache/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :todo, 7 | version: "0.1.0", 8 | elixir: "~> 1.15", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | def application do 15 | [ 16 | extra_applications: [:logger] 17 | ] 18 | end 19 | 20 | defp deps do 21 | [] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /code_samples/ch07/todo_cache/mix.lock: -------------------------------------------------------------------------------- 1 | %{} 2 | -------------------------------------------------------------------------------- /code_samples/ch07/todo_cache/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /code_samples/ch07/todo_cache/test/todo/cache_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.CacheTest do 2 | use ExUnit.Case 3 | 4 | test "server_process" do 5 | {:ok, cache} = Todo.Cache.start() 6 | bob_pid = Todo.Cache.server_process(cache, "bob") 7 | 8 | assert bob_pid != Todo.Cache.server_process(cache, "alice") 9 | assert bob_pid == Todo.Cache.server_process(cache, "bob") 10 | end 11 | 12 | test "to-do operations" do 13 | {:ok, cache} = Todo.Cache.start() 14 | alice = Todo.Cache.server_process(cache, "alice") 15 | Todo.Server.add_entry(alice, %{date: ~D[2018-12-19], title: "Dentist"}) 16 | entries = Todo.Server.entries(alice, ~D[2018-12-19]) 17 | 18 | assert [%{date: ~D[2018-12-19], title: "Dentist"}] = entries 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /code_samples/ch07/todo_cache_pooling/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /code_samples/ch07/todo_cache_pooling/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | /persist/ 6 | -------------------------------------------------------------------------------- /code_samples/ch07/todo_cache_pooling/README.md: -------------------------------------------------------------------------------- 1 | This is a demo project for Elixir in Action. 2 | 3 | To be able to compile and run it, you'll need Elixir 1.0.0 and Erlang 17.0. A `git` client should also be installed and available somewhere in the execution path. 4 | 5 | Prior to the first build, you need to call `mix deps.get`. After that, you can build and start the interactive shell by running `iex -S mix` from this project root. -------------------------------------------------------------------------------- /code_samples/ch07/todo_cache_pooling/lib/todo/cache.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Cache do 2 | use GenServer 3 | 4 | def start do 5 | GenServer.start(__MODULE__, nil) 6 | end 7 | 8 | def server_process(cache_pid, todo_list_name) do 9 | GenServer.call(cache_pid, {:server_process, todo_list_name}) 10 | end 11 | 12 | @impl GenServer 13 | def init(_) do 14 | Todo.Database.start() 15 | {:ok, %{}} 16 | end 17 | 18 | @impl GenServer 19 | def handle_call({:server_process, todo_list_name}, _, todo_servers) do 20 | case Map.fetch(todo_servers, todo_list_name) do 21 | {:ok, todo_server} -> 22 | {:reply, todo_server, todo_servers} 23 | 24 | :error -> 25 | {:ok, new_server} = Todo.Server.start(todo_list_name) 26 | 27 | { 28 | :reply, 29 | new_server, 30 | Map.put(todo_servers, todo_list_name, new_server) 31 | } 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /code_samples/ch07/todo_cache_pooling/lib/todo/database.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Database do 2 | use GenServer 3 | 4 | @db_folder "./persist" 5 | 6 | def start do 7 | GenServer.start(__MODULE__, nil, name: __MODULE__) 8 | end 9 | 10 | def store(key, data) do 11 | key 12 | |> choose_worker() 13 | |> Todo.DatabaseWorker.store(key, data) 14 | end 15 | 16 | def get(key) do 17 | key 18 | |> choose_worker() 19 | |> Todo.DatabaseWorker.get(key) 20 | end 21 | 22 | # Choosing a worker makes a request to the database server process. There we 23 | # keep the knowledge about our workers, and return the pid of the corresponding 24 | # worker. Once this is done, the caller process will talk to the worker directly. 25 | defp choose_worker(key) do 26 | GenServer.call(__MODULE__, {:choose_worker, key}) 27 | end 28 | 29 | @impl GenServer 30 | def init(_) do 31 | File.mkdir_p!(@db_folder) 32 | {:ok, start_workers()} 33 | end 34 | 35 | @impl GenServer 36 | def handle_call({:choose_worker, key}, _, workers) do 37 | worker_key = :erlang.phash2(key, 3) 38 | {:reply, Map.get(workers, worker_key), workers} 39 | end 40 | 41 | defp start_workers() do 42 | for index <- 1..3, into: %{} do 43 | {:ok, pid} = Todo.DatabaseWorker.start(@db_folder) 44 | {index - 1, pid} 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /code_samples/ch07/todo_cache_pooling/lib/todo/database_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.DatabaseWorker do 2 | use GenServer 3 | 4 | def start(db_folder) do 5 | GenServer.start(__MODULE__, db_folder) 6 | end 7 | 8 | def store(worker_pid, key, data) do 9 | GenServer.cast(worker_pid, {:store, key, data}) 10 | end 11 | 12 | def get(worker_pid, key) do 13 | GenServer.call(worker_pid, {:get, key}) 14 | end 15 | 16 | @impl GenServer 17 | def init(db_folder) do 18 | {:ok, db_folder} 19 | end 20 | 21 | @impl GenServer 22 | def handle_cast({:store, key, data}, db_folder) do 23 | db_folder 24 | |> file_name(key) 25 | |> File.write!(:erlang.term_to_binary(data)) 26 | 27 | {:noreply, db_folder} 28 | end 29 | 30 | @impl GenServer 31 | def handle_call({:get, key}, _, db_folder) do 32 | data = 33 | case File.read(file_name(db_folder, key)) do 34 | {:ok, contents} -> :erlang.binary_to_term(contents) 35 | _ -> nil 36 | end 37 | 38 | {:reply, data, db_folder} 39 | end 40 | 41 | defp file_name(db_folder, key) do 42 | Path.join(db_folder, to_string(key)) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /code_samples/ch07/todo_cache_pooling/lib/todo/list.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.List do 2 | defstruct next_id: 1, entries: %{} 3 | 4 | def new(entries \\ []) do 5 | Enum.reduce( 6 | entries, 7 | %Todo.List{}, 8 | &add_entry(&2, &1) 9 | ) 10 | end 11 | 12 | def size(todo_list) do 13 | map_size(todo_list.entries) 14 | end 15 | 16 | def add_entry(todo_list, entry) do 17 | entry = Map.put(entry, :id, todo_list.next_id) 18 | new_entries = Map.put(todo_list.entries, todo_list.next_id, entry) 19 | 20 | %Todo.List{todo_list | entries: new_entries, next_id: todo_list.next_id + 1} 21 | end 22 | 23 | def entries(todo_list, date) do 24 | todo_list.entries 25 | |> Map.values() 26 | |> Enum.filter(fn entry -> entry.date == date end) 27 | end 28 | 29 | def update_entry(todo_list, entry_id, updater_fun) do 30 | case Map.fetch(todo_list.entries, entry_id) do 31 | :error -> 32 | todo_list 33 | 34 | {:ok, old_entry} -> 35 | new_entry = updater_fun.(old_entry) 36 | new_entries = Map.put(todo_list.entries, new_entry.id, new_entry) 37 | %Todo.List{todo_list | entries: new_entries} 38 | end 39 | end 40 | 41 | def delete_entry(todo_list, entry_id) do 42 | %Todo.List{todo_list | entries: Map.delete(todo_list.entries, entry_id)} 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /code_samples/ch07/todo_cache_pooling/lib/todo/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Server do 2 | use GenServer 3 | 4 | def start(name) do 5 | GenServer.start(Todo.Server, name) 6 | end 7 | 8 | def add_entry(todo_server, new_entry) do 9 | GenServer.cast(todo_server, {:add_entry, new_entry}) 10 | end 11 | 12 | def entries(todo_server, date) do 13 | GenServer.call(todo_server, {:entries, date}) 14 | end 15 | 16 | @impl GenServer 17 | def init(name) do 18 | {:ok, {name, nil}, {:continue, :init}} 19 | end 20 | 21 | @impl GenServer 22 | def handle_continue(:init, {name, nil}) do 23 | todo_list = Todo.Database.get(name) || Todo.List.new() 24 | {:noreply, {name, todo_list}} 25 | end 26 | 27 | @impl GenServer 28 | def handle_cast({:add_entry, new_entry}, {name, todo_list}) do 29 | new_list = Todo.List.add_entry(todo_list, new_entry) 30 | Todo.Database.store(name, new_list) 31 | {:noreply, {name, new_list}} 32 | end 33 | 34 | @impl GenServer 35 | def handle_call({:entries, date}, _, {name, todo_list}) do 36 | { 37 | :reply, 38 | Todo.List.entries(todo_list, date), 39 | {name, todo_list} 40 | } 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /code_samples/ch07/todo_cache_pooling/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :todo, 7 | version: "0.1.0", 8 | elixir: "~> 1.15", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | def application do 15 | [ 16 | extra_applications: [:logger] 17 | ] 18 | end 19 | 20 | defp deps do 21 | [] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /code_samples/ch07/todo_cache_pooling/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [], [], "hexpm"}, 3 | } 4 | -------------------------------------------------------------------------------- /code_samples/ch07/todo_cache_pooling/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | File.rm_rf("./persist") 2 | File.mkdir_p("./persist") 3 | ExUnit.start() 4 | -------------------------------------------------------------------------------- /code_samples/ch07/todo_cache_pooling/test/todo/cache_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.CacheTest do 2 | use ExUnit.Case 3 | 4 | test "server_process" do 5 | {:ok, cache} = Todo.Cache.start() 6 | bob_pid = Todo.Cache.server_process(cache, "bob") 7 | 8 | assert bob_pid != Todo.Cache.server_process(cache, "alice") 9 | assert bob_pid == Todo.Cache.server_process(cache, "bob") 10 | end 11 | 12 | test "to-do operations" do 13 | {:ok, cache} = Todo.Cache.start() 14 | alice = Todo.Cache.server_process(cache, "alice") 15 | Todo.Server.add_entry(alice, %{date: ~D[2018-12-19], title: "Dentist"}) 16 | entries = Todo.Server.entries(alice, ~D[2018-12-19]) 17 | 18 | assert [%{date: ~D[2018-12-19], title: "Dentist"}] = entries 19 | end 20 | 21 | test "persistence" do 22 | {:ok, cache} = Todo.Cache.start() 23 | 24 | john = Todo.Cache.server_process(cache, "john") 25 | Todo.Server.add_entry(john, %{date: ~D[2018-12-20], title: "Shopping"}) 26 | assert 1 == length(Todo.Server.entries(john, ~D[2018-12-20])) 27 | 28 | GenServer.stop(cache) 29 | {:ok, cache} = Todo.Cache.start() 30 | 31 | entries = 32 | cache 33 | |> Todo.Cache.server_process("john") 34 | |> Todo.Server.entries(~D[2018-12-20]) 35 | 36 | assert [%{date: ~D[2018-12-20], title: "Shopping"}] = entries 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /code_samples/ch08/supervised_todo_cache/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /code_samples/ch08/supervised_todo_cache/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | /persist/ 6 | -------------------------------------------------------------------------------- /code_samples/ch08/supervised_todo_cache/README.md: -------------------------------------------------------------------------------- 1 | This is a demo project for Elixir in Action. 2 | 3 | To be able to compile and run it, you'll need Elixir 1.0.0 and Erlang 17.0. A `git` client should also be installed and available somewhere in the execution path. 4 | 5 | Prior to the first build, you need to call `mix deps.get`. After that, you can build and start the interactive shell by running `iex -S mix` from this project root. -------------------------------------------------------------------------------- /code_samples/ch08/supervised_todo_cache/lib/todo/cache.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Cache do 2 | use GenServer 3 | 4 | def start_link(_) do 5 | GenServer.start_link(__MODULE__, nil, name: __MODULE__) 6 | end 7 | 8 | def server_process(todo_list_name) do 9 | GenServer.call(__MODULE__, {:server_process, todo_list_name}) 10 | end 11 | 12 | @impl GenServer 13 | def init(_) do 14 | IO.puts("Starting to-do cache.") 15 | Todo.Database.start() 16 | {:ok, %{}} 17 | end 18 | 19 | @impl GenServer 20 | def handle_call({:server_process, todo_list_name}, _, todo_servers) do 21 | case Map.fetch(todo_servers, todo_list_name) do 22 | {:ok, todo_server} -> 23 | {:reply, todo_server, todo_servers} 24 | 25 | :error -> 26 | {:ok, new_server} = Todo.Server.start(todo_list_name) 27 | 28 | { 29 | :reply, 30 | new_server, 31 | Map.put(todo_servers, todo_list_name, new_server) 32 | } 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /code_samples/ch08/supervised_todo_cache/lib/todo/database.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Database do 2 | use GenServer 3 | 4 | @db_folder "./persist" 5 | 6 | def start do 7 | GenServer.start(__MODULE__, nil, name: __MODULE__) 8 | end 9 | 10 | def store(key, data) do 11 | key 12 | |> choose_worker() 13 | |> Todo.DatabaseWorker.store(key, data) 14 | end 15 | 16 | def get(key) do 17 | key 18 | |> choose_worker() 19 | |> Todo.DatabaseWorker.get(key) 20 | end 21 | 22 | defp choose_worker(key) do 23 | GenServer.call(__MODULE__, {:choose_worker, key}) 24 | end 25 | 26 | @impl GenServer 27 | def init(_) do 28 | IO.puts("Starting database server.") 29 | File.mkdir_p!(@db_folder) 30 | {:ok, start_workers()} 31 | end 32 | 33 | @impl GenServer 34 | def handle_call({:choose_worker, key}, _, workers) do 35 | worker_key = :erlang.phash2(key, 3) 36 | {:reply, Map.get(workers, worker_key), workers} 37 | end 38 | 39 | defp start_workers() do 40 | for index <- 1..3, into: %{} do 41 | {:ok, pid} = Todo.DatabaseWorker.start(@db_folder) 42 | {index - 1, pid} 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /code_samples/ch08/supervised_todo_cache/lib/todo/database_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.DatabaseWorker do 2 | use GenServer 3 | 4 | def start(db_folder) do 5 | GenServer.start(__MODULE__, db_folder) 6 | end 7 | 8 | def store(worker_pid, key, data) do 9 | GenServer.cast(worker_pid, {:store, key, data}) 10 | end 11 | 12 | def get(worker_pid, key) do 13 | GenServer.call(worker_pid, {:get, key}) 14 | end 15 | 16 | @impl GenServer 17 | def init(db_folder) do 18 | IO.puts("Starting database worker.") 19 | {:ok, db_folder} 20 | end 21 | 22 | @impl GenServer 23 | def handle_cast({:store, key, data}, db_folder) do 24 | db_folder 25 | |> file_name(key) 26 | |> File.write!(:erlang.term_to_binary(data)) 27 | 28 | {:noreply, db_folder} 29 | end 30 | 31 | @impl GenServer 32 | def handle_call({:get, key}, _, db_folder) do 33 | data = 34 | case File.read(file_name(db_folder, key)) do 35 | {:ok, contents} -> :erlang.binary_to_term(contents) 36 | _ -> nil 37 | end 38 | 39 | {:reply, data, db_folder} 40 | end 41 | 42 | defp file_name(db_folder, key) do 43 | Path.join(db_folder, to_string(key)) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /code_samples/ch08/supervised_todo_cache/lib/todo/list.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.List do 2 | defstruct next_id: 1, entries: %{} 3 | 4 | def new(entries \\ []) do 5 | Enum.reduce( 6 | entries, 7 | %Todo.List{}, 8 | &add_entry(&2, &1) 9 | ) 10 | end 11 | 12 | def size(todo_list) do 13 | map_size(todo_list.entries) 14 | end 15 | 16 | def add_entry(todo_list, entry) do 17 | entry = Map.put(entry, :id, todo_list.next_id) 18 | new_entries = Map.put(todo_list.entries, todo_list.next_id, entry) 19 | 20 | %Todo.List{todo_list | entries: new_entries, next_id: todo_list.next_id + 1} 21 | end 22 | 23 | def entries(todo_list, date) do 24 | todo_list.entries 25 | |> Map.values() 26 | |> Enum.filter(fn entry -> entry.date == date end) 27 | end 28 | 29 | def update_entry(todo_list, entry_id, updater_fun) do 30 | case Map.fetch(todo_list.entries, entry_id) do 31 | :error -> 32 | todo_list 33 | 34 | {:ok, old_entry} -> 35 | new_entry = updater_fun.(old_entry) 36 | new_entries = Map.put(todo_list.entries, new_entry.id, new_entry) 37 | %Todo.List{todo_list | entries: new_entries} 38 | end 39 | end 40 | 41 | def delete_entry(todo_list, entry_id) do 42 | %Todo.List{todo_list | entries: Map.delete(todo_list.entries, entry_id)} 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /code_samples/ch08/supervised_todo_cache/lib/todo/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Server do 2 | use GenServer 3 | 4 | def start(name) do 5 | GenServer.start(Todo.Server, name) 6 | end 7 | 8 | def add_entry(todo_server, new_entry) do 9 | GenServer.cast(todo_server, {:add_entry, new_entry}) 10 | end 11 | 12 | def entries(todo_server, date) do 13 | GenServer.call(todo_server, {:entries, date}) 14 | end 15 | 16 | @impl GenServer 17 | def init(name) do 18 | IO.puts("Starting to-do server for #{name}.") 19 | {:ok, {name, nil}, {:continue, :init}} 20 | end 21 | 22 | @impl GenServer 23 | def handle_continue(:init, {name, nil}) do 24 | todo_list = Todo.Database.get(name) || Todo.List.new() 25 | {:noreply, {name, todo_list}} 26 | end 27 | 28 | @impl GenServer 29 | def handle_cast({:add_entry, new_entry}, {name, todo_list}) do 30 | new_list = Todo.List.add_entry(todo_list, new_entry) 31 | Todo.Database.store(name, new_list) 32 | {:noreply, {name, new_list}} 33 | end 34 | 35 | @impl GenServer 36 | def handle_call({:entries, date}, _, {name, todo_list}) do 37 | { 38 | :reply, 39 | Todo.List.entries(todo_list, date), 40 | {name, todo_list} 41 | } 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /code_samples/ch08/supervised_todo_cache/lib/todo/system.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.System do 2 | def start_link do 3 | Supervisor.start_link( 4 | [Todo.Cache], 5 | strategy: :one_for_one 6 | ) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /code_samples/ch08/supervised_todo_cache/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :todo, 7 | version: "0.1.0", 8 | elixir: "~> 1.15", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | def application do 15 | [ 16 | extra_applications: [:logger] 17 | ] 18 | end 19 | 20 | defp deps do 21 | [] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /code_samples/ch08/supervised_todo_cache/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [], [], "hexpm"}, 3 | } 4 | -------------------------------------------------------------------------------- /code_samples/ch08/supervised_todo_cache/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | File.rm_rf("./persist") 2 | File.mkdir_p("./persist") 3 | ExUnit.start() 4 | -------------------------------------------------------------------------------- /code_samples/ch08/supervised_todo_cache/test/todo/cache_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.CacheTest do 2 | use ExUnit.Case 3 | 4 | test "server_process" do 5 | Todo.System.start_link() 6 | bob_pid = Todo.Cache.server_process("bob") 7 | 8 | assert bob_pid != Todo.Cache.server_process("alice") 9 | assert bob_pid == Todo.Cache.server_process("bob") 10 | end 11 | 12 | test "to-do operations" do 13 | Todo.System.start_link() 14 | alice = Todo.Cache.server_process("alice") 15 | Todo.Server.add_entry(alice, %{date: ~D[2018-12-19], title: "Dentist"}) 16 | entries = Todo.Server.entries(alice, ~D[2018-12-19]) 17 | 18 | assert [%{date: ~D[2018-12-19], title: "Dentist"}] = entries 19 | end 20 | 21 | test "persistence" do 22 | {:ok, supervisor} = Todo.System.start_link() 23 | 24 | john = Todo.Cache.server_process("john") 25 | Todo.Server.add_entry(john, %{date: ~D[2018-12-20], title: "Shopping"}) 26 | assert 1 == length(Todo.Server.entries(john, ~D[2018-12-20])) 27 | 28 | Supervisor.stop(supervisor) 29 | Todo.System.start_link() 30 | 31 | entries = 32 | "john" 33 | |> Todo.Cache.server_process() 34 | |> Todo.Server.entries(~D[2018-12-20]) 35 | 36 | assert [%{date: ~D[2018-12-20], title: "Shopping"}] = entries 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /code_samples/ch08/todo_links/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /code_samples/ch08/todo_links/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | /persist/ 6 | -------------------------------------------------------------------------------- /code_samples/ch08/todo_links/README.md: -------------------------------------------------------------------------------- 1 | This is a demo project for Elixir in Action. 2 | 3 | To be able to compile and run it, you'll need Elixir 1.0.0 and Erlang 17.0. A `git` client should also be installed and available somewhere in the execution path. 4 | 5 | Prior to the first build, you need to call `mix deps.get`. After that, you can build and start the interactive shell by running `iex -S mix` from this project root. -------------------------------------------------------------------------------- /code_samples/ch08/todo_links/lib/todo/cache.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Cache do 2 | use GenServer 3 | 4 | def start_link(_) do 5 | GenServer.start_link(__MODULE__, nil, name: __MODULE__) 6 | end 7 | 8 | def server_process(todo_list_name) do 9 | GenServer.call(__MODULE__, {:server_process, todo_list_name}) 10 | end 11 | 12 | @impl GenServer 13 | def init(_) do 14 | IO.puts("Starting to-do cache.") 15 | Todo.Database.start_link() 16 | {:ok, %{}} 17 | end 18 | 19 | @impl GenServer 20 | def handle_call({:server_process, todo_list_name}, _, todo_servers) do 21 | case Map.fetch(todo_servers, todo_list_name) do 22 | {:ok, todo_server} -> 23 | {:reply, todo_server, todo_servers} 24 | 25 | :error -> 26 | {:ok, new_server} = Todo.Server.start_link(todo_list_name) 27 | 28 | { 29 | :reply, 30 | new_server, 31 | Map.put(todo_servers, todo_list_name, new_server) 32 | } 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /code_samples/ch08/todo_links/lib/todo/database.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Database do 2 | use GenServer 3 | 4 | @db_folder "./persist" 5 | 6 | def start_link do 7 | GenServer.start_link(__MODULE__, nil, name: __MODULE__) 8 | end 9 | 10 | def store(key, data) do 11 | key 12 | |> choose_worker() 13 | |> Todo.DatabaseWorker.store(key, data) 14 | end 15 | 16 | def get(key) do 17 | key 18 | |> choose_worker() 19 | |> Todo.DatabaseWorker.get(key) 20 | end 21 | 22 | defp choose_worker(key) do 23 | GenServer.call(__MODULE__, {:choose_worker, key}) 24 | end 25 | 26 | @impl GenServer 27 | def init(_) do 28 | IO.puts("Starting database server.") 29 | File.mkdir_p!(@db_folder) 30 | {:ok, start_workers()} 31 | end 32 | 33 | @impl GenServer 34 | def handle_call({:choose_worker, key}, _, workers) do 35 | worker_key = :erlang.phash2(key, 3) 36 | {:reply, Map.get(workers, worker_key), workers} 37 | end 38 | 39 | defp start_workers() do 40 | for index <- 1..3, into: %{} do 41 | {:ok, pid} = Todo.DatabaseWorker.start_link(@db_folder) 42 | {index - 1, pid} 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /code_samples/ch08/todo_links/lib/todo/database_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.DatabaseWorker do 2 | use GenServer 3 | 4 | def start_link(db_folder) do 5 | GenServer.start_link(__MODULE__, db_folder) 6 | end 7 | 8 | def store(worker_pid, key, data) do 9 | GenServer.cast(worker_pid, {:store, key, data}) 10 | end 11 | 12 | def get(worker_pid, key) do 13 | GenServer.call(worker_pid, {:get, key}) 14 | end 15 | 16 | @impl GenServer 17 | def init(db_folder) do 18 | IO.puts("Starting database worker.") 19 | {:ok, db_folder} 20 | end 21 | 22 | @impl GenServer 23 | def handle_cast({:store, key, data}, db_folder) do 24 | db_folder 25 | |> file_name(key) 26 | |> File.write!(:erlang.term_to_binary(data)) 27 | 28 | {:noreply, db_folder} 29 | end 30 | 31 | @impl GenServer 32 | def handle_call({:get, key}, _, db_folder) do 33 | data = 34 | case File.read(file_name(db_folder, key)) do 35 | {:ok, contents} -> :erlang.binary_to_term(contents) 36 | _ -> nil 37 | end 38 | 39 | {:reply, data, db_folder} 40 | end 41 | 42 | defp file_name(db_folder, key) do 43 | Path.join(db_folder, to_string(key)) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /code_samples/ch08/todo_links/lib/todo/list.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.List do 2 | defstruct next_id: 1, entries: %{} 3 | 4 | def new(entries \\ []) do 5 | Enum.reduce( 6 | entries, 7 | %Todo.List{}, 8 | &add_entry(&2, &1) 9 | ) 10 | end 11 | 12 | def size(todo_list) do 13 | map_size(todo_list.entries) 14 | end 15 | 16 | def add_entry(todo_list, entry) do 17 | entry = Map.put(entry, :id, todo_list.next_id) 18 | new_entries = Map.put(todo_list.entries, todo_list.next_id, entry) 19 | 20 | %Todo.List{todo_list | entries: new_entries, next_id: todo_list.next_id + 1} 21 | end 22 | 23 | def entries(todo_list, date) do 24 | todo_list.entries 25 | |> Map.values() 26 | |> Enum.filter(fn entry -> entry.date == date end) 27 | end 28 | 29 | def update_entry(todo_list, entry_id, updater_fun) do 30 | case Map.fetch(todo_list.entries, entry_id) do 31 | :error -> 32 | todo_list 33 | 34 | {:ok, old_entry} -> 35 | new_entry = updater_fun.(old_entry) 36 | new_entries = Map.put(todo_list.entries, new_entry.id, new_entry) 37 | %Todo.List{todo_list | entries: new_entries} 38 | end 39 | end 40 | 41 | def delete_entry(todo_list, entry_id) do 42 | %Todo.List{todo_list | entries: Map.delete(todo_list.entries, entry_id)} 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /code_samples/ch08/todo_links/lib/todo/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Server do 2 | use GenServer 3 | 4 | def start_link(name) do 5 | GenServer.start_link(Todo.Server, name) 6 | end 7 | 8 | def add_entry(todo_server, new_entry) do 9 | GenServer.cast(todo_server, {:add_entry, new_entry}) 10 | end 11 | 12 | def entries(todo_server, date) do 13 | GenServer.call(todo_server, {:entries, date}) 14 | end 15 | 16 | @impl GenServer 17 | def init(name) do 18 | IO.puts("Starting to-do server for #{name}.") 19 | {:ok, {name, nil}, {:continue, :init}} 20 | end 21 | 22 | @impl GenServer 23 | def handle_continue(:init, {name, nil}) do 24 | todo_list = Todo.Database.get(name) || Todo.List.new() 25 | {:noreply, {name, todo_list}} 26 | end 27 | 28 | @impl GenServer 29 | def handle_cast({:add_entry, new_entry}, {name, todo_list}) do 30 | new_list = Todo.List.add_entry(todo_list, new_entry) 31 | Todo.Database.store(name, new_list) 32 | {:noreply, {name, new_list}} 33 | end 34 | 35 | @impl GenServer 36 | def handle_call({:entries, date}, _, {name, todo_list}) do 37 | { 38 | :reply, 39 | Todo.List.entries(todo_list, date), 40 | {name, todo_list} 41 | } 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /code_samples/ch08/todo_links/lib/todo/system.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.System do 2 | def start_link do 3 | Supervisor.start_link( 4 | [Todo.Cache], 5 | strategy: :one_for_one 6 | ) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /code_samples/ch08/todo_links/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :todo, 7 | version: "0.1.0", 8 | elixir: "~> 1.15", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | def application do 15 | [ 16 | extra_applications: [:logger] 17 | ] 18 | end 19 | 20 | defp deps do 21 | [] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /code_samples/ch08/todo_links/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [], [], "hexpm"}, 3 | } 4 | -------------------------------------------------------------------------------- /code_samples/ch08/todo_links/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | File.rm_rf("./persist") 2 | File.mkdir_p("./persist") 3 | ExUnit.start() 4 | -------------------------------------------------------------------------------- /code_samples/ch08/todo_links/test/todo/cache_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.CacheTest do 2 | use ExUnit.Case 3 | 4 | test "server_process" do 5 | Todo.System.start_link() 6 | bob_pid = Todo.Cache.server_process("bob") 7 | 8 | assert bob_pid != Todo.Cache.server_process("alice") 9 | assert bob_pid == Todo.Cache.server_process("bob") 10 | end 11 | 12 | test "to-do operations" do 13 | Todo.System.start_link() 14 | jane = Todo.Cache.server_process("jane") 15 | Todo.Server.add_entry(jane, %{date: ~D[2018-12-19], title: "Dentist"}) 16 | entries = Todo.Server.entries(jane, ~D[2018-12-19]) 17 | 18 | assert [%{date: ~D[2018-12-19], title: "Dentist"}] = entries 19 | end 20 | 21 | test "persistence" do 22 | {:ok, supervisor} = Todo.System.start_link() 23 | 24 | john = Todo.Cache.server_process("john") 25 | Todo.Server.add_entry(john, %{date: ~D[2018-12-20], title: "Shopping"}) 26 | assert 1 == length(Todo.Server.entries(john, ~D[2018-12-20])) 27 | 28 | Supervisor.stop(supervisor) 29 | Todo.System.start_link() 30 | 31 | entries = 32 | "john" 33 | |> Todo.Cache.server_process() 34 | |> Todo.Server.entries(~D[2018-12-20]) 35 | 36 | assert [%{date: ~D[2018-12-20], title: "Shopping"}] = entries 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /code_samples/ch09/dynamic_workers/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /code_samples/ch09/dynamic_workers/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | /persist/ 6 | -------------------------------------------------------------------------------- /code_samples/ch09/dynamic_workers/README.md: -------------------------------------------------------------------------------- 1 | This is a demo project for Elixir in Action. 2 | 3 | To be able to compile and run it, you'll need Elixir 1.0.0 and Erlang 17.0. A `git` client should also be installed and available somewhere in the execution path. 4 | 5 | Prior to the first build, you need to call `mix deps.get`. After that, you can build and start the interactive shell by running `iex -S mix` from this project root. -------------------------------------------------------------------------------- /code_samples/ch09/dynamic_workers/lib/todo/cache.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Cache do 2 | def start_link() do 3 | IO.puts("Starting to-do cache.") 4 | DynamicSupervisor.start_link(name: __MODULE__, strategy: :one_for_one) 5 | end 6 | 7 | def child_spec(_arg) do 8 | %{ 9 | id: __MODULE__, 10 | start: {__MODULE__, :start_link, []}, 11 | type: :supervisor 12 | } 13 | end 14 | 15 | def server_process(todo_list_name) do 16 | case start_child(todo_list_name) do 17 | {:ok, pid} -> pid 18 | {:error, {:already_started, pid}} -> pid 19 | end 20 | end 21 | 22 | defp start_child(todo_list_name) do 23 | DynamicSupervisor.start_child(__MODULE__, {Todo.Server, todo_list_name}) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /code_samples/ch09/dynamic_workers/lib/todo/database.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Database do 2 | @pool_size 3 3 | @db_folder "./persist" 4 | 5 | def start_link do 6 | IO.puts("Starting database server.") 7 | File.mkdir_p!(@db_folder) 8 | 9 | children = Enum.map(1..@pool_size, &worker_spec/1) 10 | Supervisor.start_link(children, strategy: :one_for_one) 11 | end 12 | 13 | defp worker_spec(worker_id) do 14 | default_worker_spec = {Todo.DatabaseWorker, {@db_folder, worker_id}} 15 | Supervisor.child_spec(default_worker_spec, id: worker_id) 16 | end 17 | 18 | def child_spec(_) do 19 | %{ 20 | id: __MODULE__, 21 | start: {__MODULE__, :start_link, []}, 22 | type: :supervisor 23 | } 24 | end 25 | 26 | def store(key, data) do 27 | key 28 | |> choose_worker() 29 | |> Todo.DatabaseWorker.store(key, data) 30 | end 31 | 32 | def get(key) do 33 | key 34 | |> choose_worker() 35 | |> Todo.DatabaseWorker.get(key) 36 | end 37 | 38 | defp choose_worker(key) do 39 | :erlang.phash2(key, @pool_size) + 1 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /code_samples/ch09/dynamic_workers/lib/todo/database_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.DatabaseWorker do 2 | use GenServer 3 | 4 | def start_link({db_folder, worker_id}) do 5 | GenServer.start_link( 6 | __MODULE__, 7 | db_folder, 8 | name: via_tuple(worker_id) 9 | ) 10 | end 11 | 12 | def store(worker_id, key, data) do 13 | GenServer.cast(via_tuple(worker_id), {:store, key, data}) 14 | end 15 | 16 | def get(worker_id, key) do 17 | GenServer.call(via_tuple(worker_id), {:get, key}) 18 | end 19 | 20 | defp via_tuple(worker_id) do 21 | Todo.ProcessRegistry.via_tuple({__MODULE__, worker_id}) 22 | end 23 | 24 | @impl GenServer 25 | def init(db_folder) do 26 | IO.puts("Starting database worker.") 27 | {:ok, db_folder} 28 | end 29 | 30 | @impl GenServer 31 | def handle_cast({:store, key, data}, db_folder) do 32 | db_folder 33 | |> file_name(key) 34 | |> File.write!(:erlang.term_to_binary(data)) 35 | 36 | {:noreply, db_folder} 37 | end 38 | 39 | @impl GenServer 40 | def handle_call({:get, key}, _, db_folder) do 41 | data = 42 | case File.read(file_name(db_folder, key)) do 43 | {:ok, contents} -> :erlang.binary_to_term(contents) 44 | _ -> nil 45 | end 46 | 47 | {:reply, data, db_folder} 48 | end 49 | 50 | defp file_name(db_folder, key) do 51 | Path.join(db_folder, to_string(key)) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /code_samples/ch09/dynamic_workers/lib/todo/list.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.List do 2 | defstruct next_id: 1, entries: %{} 3 | 4 | def new(entries \\ []) do 5 | Enum.reduce( 6 | entries, 7 | %Todo.List{}, 8 | &add_entry(&2, &1) 9 | ) 10 | end 11 | 12 | def size(todo_list) do 13 | map_size(todo_list.entries) 14 | end 15 | 16 | def add_entry(todo_list, entry) do 17 | entry = Map.put(entry, :id, todo_list.next_id) 18 | new_entries = Map.put(todo_list.entries, todo_list.next_id, entry) 19 | 20 | %Todo.List{todo_list | entries: new_entries, next_id: todo_list.next_id + 1} 21 | end 22 | 23 | def entries(todo_list, date) do 24 | todo_list.entries 25 | |> Map.values() 26 | |> Enum.filter(fn entry -> entry.date == date end) 27 | end 28 | 29 | def update_entry(todo_list, entry_id, updater_fun) do 30 | case Map.fetch(todo_list.entries, entry_id) do 31 | :error -> 32 | todo_list 33 | 34 | {:ok, old_entry} -> 35 | new_entry = updater_fun.(old_entry) 36 | new_entries = Map.put(todo_list.entries, new_entry.id, new_entry) 37 | %Todo.List{todo_list | entries: new_entries} 38 | end 39 | end 40 | 41 | def delete_entry(todo_list, entry_id) do 42 | %Todo.List{todo_list | entries: Map.delete(todo_list.entries, entry_id)} 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /code_samples/ch09/dynamic_workers/lib/todo/process_registry.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.ProcessRegistry do 2 | def start_link do 3 | Registry.start_link(keys: :unique, name: __MODULE__) 4 | end 5 | 6 | def via_tuple(key) do 7 | {:via, Registry, {__MODULE__, key}} 8 | end 9 | 10 | def child_spec(_) do 11 | Supervisor.child_spec( 12 | Registry, 13 | id: __MODULE__, 14 | start: {__MODULE__, :start_link, []} 15 | ) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /code_samples/ch09/dynamic_workers/lib/todo/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Server do 2 | use GenServer, restart: :temporary 3 | 4 | def start_link(name) do 5 | GenServer.start_link(Todo.Server, name, name: via_tuple(name)) 6 | end 7 | 8 | def add_entry(todo_server, new_entry) do 9 | GenServer.cast(todo_server, {:add_entry, new_entry}) 10 | end 11 | 12 | def entries(todo_server, date) do 13 | GenServer.call(todo_server, {:entries, date}) 14 | end 15 | 16 | defp via_tuple(name) do 17 | Todo.ProcessRegistry.via_tuple({__MODULE__, name}) 18 | end 19 | 20 | @impl GenServer 21 | def init(name) do 22 | IO.puts("Starting to-do server for #{name}.") 23 | {:ok, {name, nil}, {:continue, :init}} 24 | end 25 | 26 | @impl GenServer 27 | def handle_continue(:init, {name, nil}) do 28 | todo_list = Todo.Database.get(name) || Todo.List.new() 29 | {:noreply, {name, todo_list}} 30 | end 31 | 32 | @impl GenServer 33 | def handle_cast({:add_entry, new_entry}, {name, todo_list}) do 34 | new_list = Todo.List.add_entry(todo_list, new_entry) 35 | Todo.Database.store(name, new_list) 36 | {:noreply, {name, new_list}} 37 | end 38 | 39 | @impl GenServer 40 | def handle_call({:entries, date}, _, {name, todo_list}) do 41 | { 42 | :reply, 43 | Todo.List.entries(todo_list, date), 44 | {name, todo_list} 45 | } 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /code_samples/ch09/dynamic_workers/lib/todo/system.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.System do 2 | def start_link do 3 | Supervisor.start_link( 4 | [ 5 | Todo.ProcessRegistry, 6 | Todo.Database, 7 | Todo.Cache 8 | ], 9 | strategy: :one_for_one 10 | ) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /code_samples/ch09/dynamic_workers/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :todo, 7 | version: "0.1.0", 8 | elixir: "~> 1.15", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | def application do 15 | [ 16 | extra_applications: [:logger] 17 | ] 18 | end 19 | 20 | defp deps do 21 | [] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /code_samples/ch09/dynamic_workers/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [], [], "hexpm"}, 3 | } 4 | -------------------------------------------------------------------------------- /code_samples/ch09/dynamic_workers/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | File.rm_rf!("./persist") 2 | File.mkdir_p!("./persist") 3 | ExUnit.start() 4 | -------------------------------------------------------------------------------- /code_samples/ch09/dynamic_workers/test/todo/cache_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.CacheTest do 2 | use ExUnit.Case 3 | 4 | setup_all do 5 | {:ok, todo_system_pid} = Todo.System.start_link() 6 | {:ok, todo_system_pid: todo_system_pid} 7 | end 8 | 9 | test "server_process" do 10 | bob_pid = Todo.Cache.server_process("bob") 11 | 12 | assert bob_pid != Todo.Cache.server_process("alice") 13 | assert bob_pid == Todo.Cache.server_process("bob") 14 | end 15 | 16 | test "to-do operations" do 17 | jane = Todo.Cache.server_process("jane") 18 | Todo.Server.add_entry(jane, %{date: ~D[2018-12-19], title: "Dentist"}) 19 | entries = Todo.Server.entries(jane, ~D[2018-12-19]) 20 | 21 | assert [%{date: ~D[2018-12-19], title: "Dentist"}] = entries 22 | end 23 | 24 | test "persistence" do 25 | john = Todo.Cache.server_process("john") 26 | Todo.Server.add_entry(john, %{date: ~D[2018-12-20], title: "Shopping"}) 27 | assert 1 == length(Todo.Server.entries(john, ~D[2018-12-20])) 28 | 29 | Process.exit(john, :kill) 30 | 31 | entries = 32 | "john" 33 | |> Todo.Cache.server_process() 34 | |> Todo.Server.entries(~D[2018-12-20]) 35 | 36 | assert [%{date: ~D[2018-12-20], title: "Shopping"}] = entries 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /code_samples/ch09/pool_supervision/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /code_samples/ch09/pool_supervision/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | /persist/ 6 | -------------------------------------------------------------------------------- /code_samples/ch09/pool_supervision/README.md: -------------------------------------------------------------------------------- 1 | This is a demo project for Elixir in Action. 2 | 3 | To be able to compile and run it, you'll need Elixir 1.0.0 and Erlang 17.0. A `git` client should also be installed and available somewhere in the execution path. 4 | 5 | Prior to the first build, you need to call `mix deps.get`. After that, you can build and start the interactive shell by running `iex -S mix` from this project root. -------------------------------------------------------------------------------- /code_samples/ch09/pool_supervision/lib/todo/cache.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Cache do 2 | use GenServer 3 | 4 | def start_link(_) do 5 | GenServer.start_link(__MODULE__, nil, name: __MODULE__) 6 | end 7 | 8 | def server_process(todo_list_name) do 9 | GenServer.call(__MODULE__, {:server_process, todo_list_name}) 10 | end 11 | 12 | @impl GenServer 13 | def init(_) do 14 | IO.puts("Starting to-do cache.") 15 | {:ok, %{}} 16 | end 17 | 18 | @impl GenServer 19 | def handle_call({:server_process, todo_list_name}, _, todo_servers) do 20 | case Map.fetch(todo_servers, todo_list_name) do 21 | {:ok, todo_server} -> 22 | {:reply, todo_server, todo_servers} 23 | 24 | :error -> 25 | {:ok, new_server} = Todo.Server.start_link(todo_list_name) 26 | 27 | { 28 | :reply, 29 | new_server, 30 | Map.put(todo_servers, todo_list_name, new_server) 31 | } 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /code_samples/ch09/pool_supervision/lib/todo/database.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Database do 2 | @pool_size 3 3 | @db_folder "./persist" 4 | 5 | def start_link do 6 | IO.puts("Starting database server.") 7 | File.mkdir_p!(@db_folder) 8 | 9 | children = Enum.map(1..@pool_size, &worker_spec/1) 10 | Supervisor.start_link(children, strategy: :one_for_one) 11 | end 12 | 13 | defp worker_spec(worker_id) do 14 | default_worker_spec = {Todo.DatabaseWorker, {@db_folder, worker_id}} 15 | Supervisor.child_spec(default_worker_spec, id: worker_id) 16 | end 17 | 18 | def child_spec(_) do 19 | %{ 20 | id: __MODULE__, 21 | start: {__MODULE__, :start_link, []}, 22 | type: :supervisor 23 | } 24 | end 25 | 26 | def store(key, data) do 27 | key 28 | |> choose_worker() 29 | |> Todo.DatabaseWorker.store(key, data) 30 | end 31 | 32 | def get(key) do 33 | key 34 | |> choose_worker() 35 | |> Todo.DatabaseWorker.get(key) 36 | end 37 | 38 | defp choose_worker(key) do 39 | :erlang.phash2(key, @pool_size) + 1 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /code_samples/ch09/pool_supervision/lib/todo/database_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.DatabaseWorker do 2 | use GenServer 3 | 4 | def start_link({db_folder, worker_id}) do 5 | GenServer.start_link( 6 | __MODULE__, 7 | db_folder, 8 | name: via_tuple(worker_id) 9 | ) 10 | end 11 | 12 | def store(worker_id, key, data) do 13 | GenServer.cast(via_tuple(worker_id), {:store, key, data}) 14 | end 15 | 16 | def get(worker_id, key) do 17 | GenServer.call(via_tuple(worker_id), {:get, key}) 18 | end 19 | 20 | defp via_tuple(worker_id) do 21 | Todo.ProcessRegistry.via_tuple({__MODULE__, worker_id}) 22 | end 23 | 24 | @impl GenServer 25 | def init(db_folder) do 26 | IO.puts("Starting database worker.") 27 | {:ok, db_folder} 28 | end 29 | 30 | @impl GenServer 31 | def handle_cast({:store, key, data}, db_folder) do 32 | db_folder 33 | |> file_name(key) 34 | |> File.write!(:erlang.term_to_binary(data)) 35 | 36 | {:noreply, db_folder} 37 | end 38 | 39 | @impl GenServer 40 | def handle_call({:get, key}, _, db_folder) do 41 | data = 42 | case File.read(file_name(db_folder, key)) do 43 | {:ok, contents} -> :erlang.binary_to_term(contents) 44 | _ -> nil 45 | end 46 | 47 | {:reply, data, db_folder} 48 | end 49 | 50 | defp file_name(db_folder, key) do 51 | Path.join(db_folder, to_string(key)) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /code_samples/ch09/pool_supervision/lib/todo/list.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.List do 2 | defstruct next_id: 1, entries: %{} 3 | 4 | def new(entries \\ []) do 5 | Enum.reduce( 6 | entries, 7 | %Todo.List{}, 8 | &add_entry(&2, &1) 9 | ) 10 | end 11 | 12 | def size(todo_list) do 13 | map_size(todo_list.entries) 14 | end 15 | 16 | def add_entry(todo_list, entry) do 17 | entry = Map.put(entry, :id, todo_list.next_id) 18 | new_entries = Map.put(todo_list.entries, todo_list.next_id, entry) 19 | 20 | %Todo.List{todo_list | entries: new_entries, next_id: todo_list.next_id + 1} 21 | end 22 | 23 | def entries(todo_list, date) do 24 | todo_list.entries 25 | |> Map.values() 26 | |> Enum.filter(fn entry -> entry.date == date end) 27 | end 28 | 29 | def update_entry(todo_list, entry_id, updater_fun) do 30 | case Map.fetch(todo_list.entries, entry_id) do 31 | :error -> 32 | todo_list 33 | 34 | {:ok, old_entry} -> 35 | new_entry = updater_fun.(old_entry) 36 | new_entries = Map.put(todo_list.entries, new_entry.id, new_entry) 37 | %Todo.List{todo_list | entries: new_entries} 38 | end 39 | end 40 | 41 | def delete_entry(todo_list, entry_id) do 42 | %Todo.List{todo_list | entries: Map.delete(todo_list.entries, entry_id)} 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /code_samples/ch09/pool_supervision/lib/todo/process_registry.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.ProcessRegistry do 2 | def start_link do 3 | Registry.start_link(keys: :unique, name: __MODULE__) 4 | end 5 | 6 | def via_tuple(key) do 7 | {:via, Registry, {__MODULE__, key}} 8 | end 9 | 10 | def child_spec(_) do 11 | Supervisor.child_spec( 12 | Registry, 13 | id: __MODULE__, 14 | start: {__MODULE__, :start_link, []} 15 | ) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /code_samples/ch09/pool_supervision/lib/todo/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Server do 2 | use GenServer 3 | 4 | def start_link(name) do 5 | GenServer.start_link(Todo.Server, name) 6 | end 7 | 8 | def add_entry(todo_server, new_entry) do 9 | GenServer.cast(todo_server, {:add_entry, new_entry}) 10 | end 11 | 12 | def entries(todo_server, date) do 13 | GenServer.call(todo_server, {:entries, date}) 14 | end 15 | 16 | @impl GenServer 17 | def init(name) do 18 | IO.puts("Starting to-do server for #{name}.") 19 | {:ok, {name, nil}, {:continue, :init}} 20 | end 21 | 22 | @impl GenServer 23 | def handle_continue(:init, {name, nil}) do 24 | todo_list = Todo.Database.get(name) || Todo.List.new() 25 | {:noreply, {name, todo_list}} 26 | end 27 | 28 | @impl GenServer 29 | def handle_cast({:add_entry, new_entry}, {name, todo_list}) do 30 | new_list = Todo.List.add_entry(todo_list, new_entry) 31 | Todo.Database.store(name, new_list) 32 | {:noreply, {name, new_list}} 33 | end 34 | 35 | @impl GenServer 36 | def handle_call({:entries, date}, _, {name, todo_list}) do 37 | { 38 | :reply, 39 | Todo.List.entries(todo_list, date), 40 | {name, todo_list} 41 | } 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /code_samples/ch09/pool_supervision/lib/todo/system.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.System do 2 | def start_link do 3 | Supervisor.start_link( 4 | [ 5 | Todo.ProcessRegistry, 6 | Todo.Database, 7 | Todo.Cache 8 | ], 9 | strategy: :one_for_one 10 | ) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /code_samples/ch09/pool_supervision/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :todo, 7 | version: "0.1.0", 8 | elixir: "~> 1.15", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | def application do 15 | [ 16 | extra_applications: [:logger] 17 | ] 18 | end 19 | 20 | defp deps do 21 | [] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /code_samples/ch09/pool_supervision/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [], [], "hexpm"}, 3 | } 4 | -------------------------------------------------------------------------------- /code_samples/ch09/pool_supervision/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | File.rm_rf!("./persist") 2 | File.mkdir_p!("./persist") 3 | ExUnit.start() 4 | -------------------------------------------------------------------------------- /code_samples/ch09/pool_supervision/test/todo/cache_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.CacheTest do 2 | use ExUnit.Case 3 | 4 | setup_all do 5 | {:ok, todo_system_pid} = Todo.System.start_link() 6 | {:ok, todo_system_pid: todo_system_pid} 7 | end 8 | 9 | test "server_process" do 10 | bob_pid = Todo.Cache.server_process("bob") 11 | 12 | assert bob_pid != Todo.Cache.server_process("alice") 13 | assert bob_pid == Todo.Cache.server_process("bob") 14 | end 15 | 16 | test "to-do operations" do 17 | jane = Todo.Cache.server_process("jane") 18 | Todo.Server.add_entry(jane, %{date: ~D[2018-12-19], title: "Dentist"}) 19 | entries = Todo.Server.entries(jane, ~D[2018-12-19]) 20 | 21 | assert [%{date: ~D[2018-12-19], title: "Dentist"}] = entries 22 | end 23 | 24 | test "persistence", context do 25 | john = Todo.Cache.server_process("john") 26 | Todo.Server.add_entry(john, %{date: ~D[2018-12-20], title: "Shopping"}) 27 | assert 1 == length(Todo.Server.entries(john, ~D[2018-12-20])) 28 | 29 | Supervisor.terminate_child(context.todo_system_pid, Todo.Cache) 30 | Supervisor.restart_child(context.todo_system_pid, Todo.Cache) 31 | 32 | entries = 33 | "john" 34 | |> Todo.Cache.server_process() 35 | |> Todo.Server.entries(~D[2018-12-20]) 36 | 37 | assert [%{date: ~D[2018-12-20], title: "Shopping"}] = entries 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /code_samples/ch09/supervise_database/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /code_samples/ch09/supervise_database/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | /persist/ 6 | -------------------------------------------------------------------------------- /code_samples/ch09/supervise_database/README.md: -------------------------------------------------------------------------------- 1 | This is a demo project for Elixir in Action. 2 | 3 | To be able to compile and run it, you'll need Elixir 1.0.0 and Erlang 17.0. A `git` client should also be installed and available somewhere in the execution path. 4 | 5 | Prior to the first build, you need to call `mix deps.get`. After that, you can build and start the interactive shell by running `iex -S mix` from this project root. -------------------------------------------------------------------------------- /code_samples/ch09/supervise_database/lib/todo/cache.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Cache do 2 | use GenServer 3 | 4 | def start_link(_) do 5 | GenServer.start_link(__MODULE__, nil, name: __MODULE__) 6 | end 7 | 8 | def server_process(todo_list_name) do 9 | GenServer.call(__MODULE__, {:server_process, todo_list_name}) 10 | end 11 | 12 | @impl GenServer 13 | def init(_) do 14 | IO.puts("Starting to-do cache.") 15 | {:ok, %{}} 16 | end 17 | 18 | @impl GenServer 19 | def handle_call({:server_process, todo_list_name}, _, todo_servers) do 20 | case Map.fetch(todo_servers, todo_list_name) do 21 | {:ok, todo_server} -> 22 | {:reply, todo_server, todo_servers} 23 | 24 | :error -> 25 | {:ok, new_server} = Todo.Server.start_link(todo_list_name) 26 | 27 | { 28 | :reply, 29 | new_server, 30 | Map.put(todo_servers, todo_list_name, new_server) 31 | } 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /code_samples/ch09/supervise_database/lib/todo/database.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Database do 2 | use GenServer 3 | 4 | @db_folder "./persist" 5 | 6 | def start_link(_) do 7 | GenServer.start_link(__MODULE__, nil, name: __MODULE__) 8 | end 9 | 10 | def store(key, data) do 11 | key 12 | |> choose_worker() 13 | |> Todo.DatabaseWorker.store(key, data) 14 | end 15 | 16 | def get(key) do 17 | key 18 | |> choose_worker() 19 | |> Todo.DatabaseWorker.get(key) 20 | end 21 | 22 | defp choose_worker(key) do 23 | GenServer.call(__MODULE__, {:choose_worker, key}) 24 | end 25 | 26 | @impl GenServer 27 | def init(_) do 28 | IO.puts("Starting database server.") 29 | File.mkdir_p!(@db_folder) 30 | {:ok, start_workers()} 31 | end 32 | 33 | @impl GenServer 34 | def handle_call({:choose_worker, key}, _, workers) do 35 | worker_key = :erlang.phash2(key, 3) 36 | {:reply, Map.get(workers, worker_key), workers} 37 | end 38 | 39 | defp start_workers() do 40 | for index <- 1..3, into: %{} do 41 | {:ok, pid} = Todo.DatabaseWorker.start_link(@db_folder) 42 | {index - 1, pid} 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /code_samples/ch09/supervise_database/lib/todo/database_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.DatabaseWorker do 2 | use GenServer 3 | 4 | def start_link(db_folder) do 5 | GenServer.start_link(__MODULE__, db_folder) 6 | end 7 | 8 | def store(worker_pid, key, data) do 9 | GenServer.cast(worker_pid, {:store, key, data}) 10 | end 11 | 12 | def get(worker_pid, key) do 13 | GenServer.call(worker_pid, {:get, key}) 14 | end 15 | 16 | @impl GenServer 17 | def init(db_folder) do 18 | IO.puts("Starting database worker.") 19 | {:ok, db_folder} 20 | end 21 | 22 | @impl GenServer 23 | def handle_cast({:store, key, data}, db_folder) do 24 | db_folder 25 | |> file_name(key) 26 | |> File.write!(:erlang.term_to_binary(data)) 27 | 28 | {:noreply, db_folder} 29 | end 30 | 31 | @impl GenServer 32 | def handle_call({:get, key}, _, db_folder) do 33 | data = 34 | case File.read(file_name(db_folder, key)) do 35 | {:ok, contents} -> :erlang.binary_to_term(contents) 36 | _ -> nil 37 | end 38 | 39 | {:reply, data, db_folder} 40 | end 41 | 42 | defp file_name(db_folder, key) do 43 | Path.join(db_folder, to_string(key)) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /code_samples/ch09/supervise_database/lib/todo/list.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.List do 2 | defstruct next_id: 1, entries: %{} 3 | 4 | def new(entries \\ []) do 5 | Enum.reduce( 6 | entries, 7 | %Todo.List{}, 8 | &add_entry(&2, &1) 9 | ) 10 | end 11 | 12 | def size(todo_list) do 13 | map_size(todo_list.entries) 14 | end 15 | 16 | def add_entry(todo_list, entry) do 17 | entry = Map.put(entry, :id, todo_list.next_id) 18 | new_entries = Map.put(todo_list.entries, todo_list.next_id, entry) 19 | 20 | %Todo.List{todo_list | entries: new_entries, next_id: todo_list.next_id + 1} 21 | end 22 | 23 | def entries(todo_list, date) do 24 | todo_list.entries 25 | |> Map.values() 26 | |> Enum.filter(fn entry -> entry.date == date end) 27 | end 28 | 29 | def update_entry(todo_list, entry_id, updater_fun) do 30 | case Map.fetch(todo_list.entries, entry_id) do 31 | :error -> 32 | todo_list 33 | 34 | {:ok, old_entry} -> 35 | new_entry = updater_fun.(old_entry) 36 | new_entries = Map.put(todo_list.entries, new_entry.id, new_entry) 37 | %Todo.List{todo_list | entries: new_entries} 38 | end 39 | end 40 | 41 | def delete_entry(todo_list, entry_id) do 42 | %Todo.List{todo_list | entries: Map.delete(todo_list.entries, entry_id)} 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /code_samples/ch09/supervise_database/lib/todo/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Server do 2 | use GenServer 3 | 4 | def start_link(name) do 5 | GenServer.start_link(Todo.Server, name) 6 | end 7 | 8 | def add_entry(todo_server, new_entry) do 9 | GenServer.cast(todo_server, {:add_entry, new_entry}) 10 | end 11 | 12 | def entries(todo_server, date) do 13 | GenServer.call(todo_server, {:entries, date}) 14 | end 15 | 16 | @impl GenServer 17 | def init(name) do 18 | IO.puts("Starting to-do server for #{name}.") 19 | {:ok, {name, nil}, {:continue, :init}} 20 | end 21 | 22 | @impl GenServer 23 | def handle_continue(:init, {name, nil}) do 24 | todo_list = Todo.Database.get(name) || Todo.List.new() 25 | {:noreply, {name, todo_list}} 26 | end 27 | 28 | @impl GenServer 29 | def handle_cast({:add_entry, new_entry}, {name, todo_list}) do 30 | new_list = Todo.List.add_entry(todo_list, new_entry) 31 | Todo.Database.store(name, new_list) 32 | {:noreply, {name, new_list}} 33 | end 34 | 35 | @impl GenServer 36 | def handle_call({:entries, date}, _, {name, todo_list}) do 37 | { 38 | :reply, 39 | Todo.List.entries(todo_list, date), 40 | {name, todo_list} 41 | } 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /code_samples/ch09/supervise_database/lib/todo/system.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.System do 2 | def start_link do 3 | Supervisor.start_link( 4 | [ 5 | Todo.Database, 6 | Todo.Cache 7 | ], 8 | strategy: :one_for_one 9 | ) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /code_samples/ch09/supervise_database/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :todo, 7 | version: "0.1.0", 8 | elixir: "~> 1.15", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | def application do 15 | [ 16 | extra_applications: [:logger] 17 | ] 18 | end 19 | 20 | defp deps do 21 | [] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /code_samples/ch09/supervise_database/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [], [], "hexpm"}, 3 | } 4 | -------------------------------------------------------------------------------- /code_samples/ch09/supervise_database/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | File.rm_rf!("./persist") 2 | File.mkdir_p!("./persist") 3 | ExUnit.start() 4 | -------------------------------------------------------------------------------- /code_samples/ch09/supervise_database/test/todo/cache_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.CacheTest do 2 | use ExUnit.Case 3 | 4 | test "server_process" do 5 | Todo.System.start_link() 6 | bob_pid = Todo.Cache.server_process("bob") 7 | 8 | assert bob_pid != Todo.Cache.server_process("alice") 9 | assert bob_pid == Todo.Cache.server_process("bob") 10 | end 11 | 12 | test "to-do operations" do 13 | Todo.System.start_link() 14 | jane = Todo.Cache.server_process("jane") 15 | Todo.Server.add_entry(jane, %{date: ~D[2018-12-19], title: "Dentist"}) 16 | entries = Todo.Server.entries(jane, ~D[2018-12-19]) 17 | 18 | assert [%{date: ~D[2018-12-19], title: "Dentist"}] = entries 19 | end 20 | 21 | test "persistence" do 22 | {:ok, supervisor} = Todo.System.start_link() 23 | 24 | john = Todo.Cache.server_process("john") 25 | Todo.Server.add_entry(john, %{date: ~D[2018-12-20], title: "Shopping"}) 26 | assert 1 == length(Todo.Server.entries(john, ~D[2018-12-20])) 27 | 28 | Supervisor.stop(supervisor) 29 | Todo.System.start_link() 30 | 31 | entries = 32 | "john" 33 | |> Todo.Cache.server_process() 34 | |> Todo.Server.entries(~D[2018-12-20]) 35 | 36 | assert [%{date: ~D[2018-12-20], title: "Shopping"}] = entries 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /code_samples/ch10/key_value/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /code_samples/ch10/key_value/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | -------------------------------------------------------------------------------- /code_samples/ch10/key_value/README.md: -------------------------------------------------------------------------------- 1 | This is a demo project for Elixir in Action. 2 | 3 | To be able to compile and run it, you'll need Elixir 1.0.0 and Erlang 17.0. A `git` client should also be installed and available somewhere in the execution path. 4 | 5 | Prior to the first build, you need to call `mix deps.get`. After that, you can build and start the interactive shell by running `iex -S mix` from this project root. -------------------------------------------------------------------------------- /code_samples/ch10/key_value/lib/ets_key_value.ex: -------------------------------------------------------------------------------- 1 | defmodule EtsKeyValue do 2 | use GenServer 3 | 4 | def start_link do 5 | GenServer.start_link(__MODULE__, nil, name: __MODULE__) 6 | end 7 | 8 | def put(key, value) do 9 | :ets.insert(__MODULE__, {key, value}) 10 | end 11 | 12 | def get(key) do 13 | case :ets.lookup(__MODULE__, key) do 14 | [{^key, value}] -> value 15 | [] -> nil 16 | end 17 | end 18 | 19 | @impl GenServer 20 | def init(_) do 21 | :ets.new(__MODULE__, [:named_table, :public, write_concurrency: true]) 22 | {:ok, nil} 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /code_samples/ch10/key_value/lib/key_value.ex: -------------------------------------------------------------------------------- 1 | defmodule KeyValue do 2 | use GenServer 3 | 4 | def start_link do 5 | GenServer.start_link(__MODULE__, nil, name: __MODULE__) 6 | end 7 | 8 | def put(key, value) do 9 | GenServer.cast(__MODULE__, {:put, key, value}) 10 | end 11 | 12 | def get(key) do 13 | GenServer.call(__MODULE__, {:get, key}) 14 | end 15 | 16 | @impl GenServer 17 | def init(_) do 18 | {:ok, %{}} 19 | end 20 | 21 | @impl GenServer 22 | def handle_cast({:put, key, value}, store) do 23 | {:noreply, Map.put(store, key, value)} 24 | end 25 | 26 | @impl GenServer 27 | def handle_call({:get, key}, _, store) do 28 | {:reply, Map.get(store, key), store} 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /code_samples/ch10/key_value/lib/web_server.ex: -------------------------------------------------------------------------------- 1 | defmodule WebServer do 2 | def index do 3 | Process.sleep(100) 4 | 5 | "..." 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /code_samples/ch10/key_value/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule KeyValue.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :key_value, 7 | version: "0.1.0", 8 | elixir: "~> 1.6", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | def application do 15 | [ 16 | extra_applications: [:logger] 17 | ] 18 | end 19 | 20 | defp deps do 21 | [] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /code_samples/ch10/key_value/test/key_value_test.exs: -------------------------------------------------------------------------------- 1 | defmodule KeyValueTest do 2 | use ExUnit.Case 3 | 4 | test "key value" do 5 | KeyValue.start_link() 6 | KeyValue.put(:foo, 1) 7 | KeyValue.put(:bar, 2) 8 | 9 | assert KeyValue.get(:foo) == 1 10 | assert KeyValue.get(:bar) == 2 11 | end 12 | 13 | test "ets key value" do 14 | EtsKeyValue.start_link() 15 | EtsKeyValue.put(:foo, 1) 16 | EtsKeyValue.put(:bar, 2) 17 | 18 | assert EtsKeyValue.get(:foo) == 1 19 | assert EtsKeyValue.get(:bar) == 2 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /code_samples/ch10/key_value/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /code_samples/ch10/process_registry/ets.ex: -------------------------------------------------------------------------------- 1 | defmodule SimpleRegistry do 2 | use GenServer 3 | 4 | def start_link do 5 | GenServer.start_link(__MODULE__, nil, name: __MODULE__) 6 | end 7 | 8 | def register(key) do 9 | # We're linking to the registry server first, to avoid possible race condition.Note that it's therefore possible 10 | # that a caller process is linked, even though the registration fails. We can't simply unlink on a failing 11 | # registration, since a process might be registered under some other term. To properly solve this, we'd need another 12 | # ETS table to keep track of whether a process is already registered under some other term. To keep things simple, 13 | # this is not done here. For a proper implementation, you can study the Registry code at 14 | # https://github.com/elixir-lang/elixir/blob/master/lib/elixir/lib/registry.ex 15 | Process.link(Process.whereis(__MODULE__)) 16 | 17 | if :ets.insert_new(__MODULE__, {key, self()}) do 18 | :ok 19 | else 20 | :error 21 | end 22 | end 23 | 24 | def whereis(key) do 25 | case :ets.lookup(__MODULE__, key) do 26 | [{^key, pid}] -> pid 27 | [] -> nil 28 | end 29 | end 30 | 31 | @impl GenServer 32 | def init(_) do 33 | Process.flag(:trap_exit, true) 34 | :ets.new(__MODULE__, [:named_table, :public, read_concurrency: true, write_concurrency: true]) 35 | {:ok, nil} 36 | end 37 | 38 | @impl GenServer 39 | def handle_info({:EXIT, pid, _reason}, state) do 40 | :ets.match_delete(__MODULE__, {:_, pid}) 41 | {:noreply, state} 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /code_samples/ch10/process_registry/gen_server.ex: -------------------------------------------------------------------------------- 1 | defmodule SimpleRegistry do 2 | use GenServer 3 | 4 | def start_link do 5 | GenServer.start_link(__MODULE__, nil, name: __MODULE__) 6 | end 7 | 8 | def register(key) do 9 | GenServer.call(__MODULE__, {:register, key, self()}) 10 | end 11 | 12 | def whereis(key) do 13 | GenServer.call(__MODULE__, {:whereis, key}) 14 | end 15 | 16 | @impl GenServer 17 | def init(_) do 18 | Process.flag(:trap_exit, true) 19 | {:ok, %{}} 20 | end 21 | 22 | @impl GenServer 23 | def handle_call({:register, key, pid}, _, process_registry) do 24 | case Map.get(process_registry, key) do 25 | nil -> 26 | Process.link(pid) 27 | {:reply, :ok, Map.put(process_registry, key, pid)} 28 | 29 | _ -> 30 | {:reply, :error, process_registry} 31 | end 32 | end 33 | 34 | @impl GenServer 35 | def handle_call({:whereis, key}, _, process_registry) do 36 | {:reply, Map.get(process_registry, key), process_registry} 37 | end 38 | 39 | @impl GenServer 40 | def handle_info({:EXIT, pid, _reason}, process_registry) do 41 | {:noreply, deregister_pid(process_registry, pid)} 42 | end 43 | 44 | defp deregister_pid(process_registry, pid) do 45 | # We'll walk through each {key, value} item, and keep those elements whose 46 | # value is different to the provided pid. 47 | process_registry 48 | |> Enum.reject(fn {_key, registered_process} -> registered_process == pid end) 49 | |> Enum.into(%{}) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /code_samples/ch10/process_registry/test/tests.exs: -------------------------------------------------------------------------------- 1 | Code.load_file("#{__DIR__}/../../../test_helper.exs") 2 | 3 | defmodule Test do 4 | use ExUnit.Case, async: false 5 | import TestHelper 6 | 7 | test_script "gen_server" do 8 | SimpleRegistry.start_link() 9 | assert SimpleRegistry.register("foo") == :ok 10 | assert SimpleRegistry.register("foo") == :error 11 | assert SimpleRegistry.whereis("foo") == self() 12 | assert SimpleRegistry.whereis("bar") == nil 13 | 14 | {:ok, pid} = Agent.start_link(fn -> SimpleRegistry.register("bar") end) 15 | assert SimpleRegistry.whereis("bar") == pid 16 | Agent.stop(pid) 17 | Process.sleep(100) 18 | assert SimpleRegistry.whereis("bar") == nil 19 | end 20 | 21 | test_script "ets" do 22 | SimpleRegistry.start_link() 23 | assert SimpleRegistry.register("foo") == :ok 24 | assert SimpleRegistry.register("foo") == :error 25 | assert SimpleRegistry.whereis("foo") == self() 26 | assert SimpleRegistry.whereis("bar") == nil 27 | 28 | {:ok, pid} = Agent.start_link(fn -> SimpleRegistry.register("bar") end) 29 | assert SimpleRegistry.whereis("bar") == pid 30 | Agent.stop(pid) 31 | Process.sleep(100) 32 | assert SimpleRegistry.whereis("bar") == nil 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_agent/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_agent/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | /persist/ 6 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_agent/README.md: -------------------------------------------------------------------------------- 1 | This is a demo project for Elixir in Action. 2 | 3 | To be able to compile and run it, you'll need Elixir 1.0.0 and Erlang 17.0. A `git` client should also be installed and available somewhere in the execution path. 4 | 5 | Prior to the first build, you need to call `mix deps.get`. After that, you can build and start the interactive shell by running `iex -S mix` from this project root. -------------------------------------------------------------------------------- /code_samples/ch10/todo_agent/lib/todo/cache.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Cache do 2 | def start_link() do 3 | IO.puts("Starting to-do cache.") 4 | DynamicSupervisor.start_link(name: __MODULE__, strategy: :one_for_one) 5 | end 6 | 7 | def child_spec(_arg) do 8 | %{ 9 | id: __MODULE__, 10 | start: {__MODULE__, :start_link, []}, 11 | type: :supervisor 12 | } 13 | end 14 | 15 | def server_process(todo_list_name) do 16 | case start_child(todo_list_name) do 17 | {:ok, pid} -> pid 18 | {:error, {:already_started, pid}} -> pid 19 | end 20 | end 21 | 22 | defp start_child(todo_list_name) do 23 | DynamicSupervisor.start_child(__MODULE__, {Todo.Server, todo_list_name}) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_agent/lib/todo/database.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Database do 2 | @pool_size 3 3 | @db_folder "./persist" 4 | 5 | def start_link do 6 | IO.puts("Starting database server.") 7 | File.mkdir_p!(@db_folder) 8 | 9 | children = Enum.map(1..@pool_size, &worker_spec/1) 10 | Supervisor.start_link(children, strategy: :one_for_one) 11 | end 12 | 13 | defp worker_spec(worker_id) do 14 | default_worker_spec = {Todo.DatabaseWorker, {@db_folder, worker_id}} 15 | Supervisor.child_spec(default_worker_spec, id: worker_id) 16 | end 17 | 18 | def child_spec(_) do 19 | %{ 20 | id: __MODULE__, 21 | start: {__MODULE__, :start_link, []}, 22 | type: :supervisor 23 | } 24 | end 25 | 26 | def store(key, data) do 27 | key 28 | |> choose_worker() 29 | |> Todo.DatabaseWorker.store(key, data) 30 | end 31 | 32 | def get(key) do 33 | key 34 | |> choose_worker() 35 | |> Todo.DatabaseWorker.get(key) 36 | end 37 | 38 | defp choose_worker(key) do 39 | :erlang.phash2(key, @pool_size) + 1 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_agent/lib/todo/database_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.DatabaseWorker do 2 | use GenServer 3 | 4 | def start_link({db_folder, worker_id}) do 5 | GenServer.start_link( 6 | __MODULE__, 7 | db_folder, 8 | name: via_tuple(worker_id) 9 | ) 10 | end 11 | 12 | def store(worker_id, key, data) do 13 | GenServer.cast(via_tuple(worker_id), {:store, key, data}) 14 | end 15 | 16 | def get(worker_id, key) do 17 | GenServer.call(via_tuple(worker_id), {:get, key}) 18 | end 19 | 20 | defp via_tuple(worker_id) do 21 | Todo.ProcessRegistry.via_tuple({__MODULE__, worker_id}) 22 | end 23 | 24 | @impl GenServer 25 | def init(db_folder) do 26 | IO.puts("Starting database worker.") 27 | {:ok, db_folder} 28 | end 29 | 30 | @impl GenServer 31 | def handle_cast({:store, key, data}, db_folder) do 32 | db_folder 33 | |> file_name(key) 34 | |> File.write!(:erlang.term_to_binary(data)) 35 | 36 | {:noreply, db_folder} 37 | end 38 | 39 | @impl GenServer 40 | def handle_call({:get, key}, _, db_folder) do 41 | data = 42 | case File.read(file_name(db_folder, key)) do 43 | {:ok, contents} -> :erlang.binary_to_term(contents) 44 | _ -> nil 45 | end 46 | 47 | {:reply, data, db_folder} 48 | end 49 | 50 | defp file_name(db_folder, key) do 51 | Path.join(db_folder, to_string(key)) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_agent/lib/todo/list.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.List do 2 | defstruct next_id: 1, entries: %{} 3 | 4 | def new(entries \\ []) do 5 | Enum.reduce( 6 | entries, 7 | %Todo.List{}, 8 | &add_entry(&2, &1) 9 | ) 10 | end 11 | 12 | def size(todo_list) do 13 | map_size(todo_list.entries) 14 | end 15 | 16 | def add_entry(todo_list, entry) do 17 | entry = Map.put(entry, :id, todo_list.next_id) 18 | new_entries = Map.put(todo_list.entries, todo_list.next_id, entry) 19 | 20 | %Todo.List{todo_list | entries: new_entries, next_id: todo_list.next_id + 1} 21 | end 22 | 23 | def entries(todo_list, date) do 24 | todo_list.entries 25 | |> Map.values() 26 | |> Enum.filter(fn entry -> entry.date == date end) 27 | end 28 | 29 | def update_entry(todo_list, entry_id, updater_fun) do 30 | case Map.fetch(todo_list.entries, entry_id) do 31 | :error -> 32 | todo_list 33 | 34 | {:ok, old_entry} -> 35 | new_entry = updater_fun.(old_entry) 36 | new_entries = Map.put(todo_list.entries, new_entry.id, new_entry) 37 | %Todo.List{todo_list | entries: new_entries} 38 | end 39 | end 40 | 41 | def delete_entry(todo_list, entry_id) do 42 | %Todo.List{todo_list | entries: Map.delete(todo_list.entries, entry_id)} 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_agent/lib/todo/metrics.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Metrics do 2 | use Task 3 | 4 | def start_link(_arg) do 5 | Task.start_link(&loop/0) 6 | end 7 | 8 | defp loop() do 9 | Process.sleep(:timer.seconds(10)) 10 | IO.inspect(collect_metrics()) 11 | loop() 12 | end 13 | 14 | defp collect_metrics() do 15 | [ 16 | memory_usage: :erlang.memory(:total), 17 | process_count: :erlang.system_info(:process_count) 18 | ] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_agent/lib/todo/process_registry.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.ProcessRegistry do 2 | def start_link do 3 | Registry.start_link(keys: :unique, name: __MODULE__) 4 | end 5 | 6 | def via_tuple(key) do 7 | {:via, Registry, {__MODULE__, key}} 8 | end 9 | 10 | def child_spec(_) do 11 | Supervisor.child_spec( 12 | Registry, 13 | id: __MODULE__, 14 | start: {__MODULE__, :start_link, []} 15 | ) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_agent/lib/todo/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Server do 2 | use Agent, restart: :temporary 3 | 4 | def start_link(name) do 5 | Agent.start_link( 6 | fn -> 7 | IO.puts("Starting to-do server for #{name}") 8 | {name, Todo.Database.get(name) || Todo.List.new()} 9 | end, 10 | name: via_tuple(name) 11 | ) 12 | end 13 | 14 | def add_entry(todo_server, new_entry) do 15 | Agent.cast(todo_server, fn {name, todo_list} -> 16 | new_list = Todo.List.add_entry(todo_list, new_entry) 17 | Todo.Database.store(name, new_list) 18 | {name, new_list} 19 | end) 20 | end 21 | 22 | def entries(todo_server, date) do 23 | Agent.get(todo_server, fn {_name, todo_list} -> Todo.List.entries(todo_list, date) end) 24 | end 25 | 26 | defp via_tuple(name) do 27 | Todo.ProcessRegistry.via_tuple({__MODULE__, name}) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_agent/lib/todo/system.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.System do 2 | def start_link do 3 | Supervisor.start_link( 4 | [ 5 | Todo.Metrics, 6 | Todo.ProcessRegistry, 7 | Todo.Database, 8 | Todo.Cache 9 | ], 10 | strategy: :one_for_one 11 | ) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_agent/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :todo, 7 | version: "0.1.0", 8 | elixir: "~> 1.15", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | def application do 15 | [ 16 | extra_applications: [:logger] 17 | ] 18 | end 19 | 20 | defp deps do 21 | [] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_agent/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [], [], "hexpm"}, 3 | } 4 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_agent/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | File.rm_rf!("./persist") 2 | File.mkdir_p!("./persist") 3 | ExUnit.start() 4 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_agent/test/todo/cache_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.CacheTest do 2 | use ExUnit.Case 3 | 4 | setup_all do 5 | {:ok, todo_system_pid} = Todo.System.start_link() 6 | {:ok, todo_system_pid: todo_system_pid} 7 | end 8 | 9 | test "server_process" do 10 | bob_pid = Todo.Cache.server_process("bob") 11 | 12 | assert bob_pid != Todo.Cache.server_process("alice") 13 | assert bob_pid == Todo.Cache.server_process("bob") 14 | end 15 | 16 | test "to-do operations" do 17 | jane = Todo.Cache.server_process("jane") 18 | Todo.Server.add_entry(jane, %{date: ~D[2018-12-19], title: "Dentist"}) 19 | entries = Todo.Server.entries(jane, ~D[2018-12-19]) 20 | 21 | assert [%{date: ~D[2018-12-19], title: "Dentist"}] = entries 22 | end 23 | 24 | test "persistence" do 25 | john = Todo.Cache.server_process("john") 26 | Todo.Server.add_entry(john, %{date: ~D[2018-12-20], title: "Shopping"}) 27 | assert 1 == length(Todo.Server.entries(john, ~D[2018-12-20])) 28 | 29 | Process.exit(john, :kill) 30 | 31 | entries = 32 | "john" 33 | |> Todo.Cache.server_process() 34 | |> Todo.Server.entries(~D[2018-12-20]) 35 | 36 | assert [%{date: ~D[2018-12-20], title: "Shopping"}] = entries 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_cache_expiry/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_cache_expiry/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | /persist/ 6 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_cache_expiry/README.md: -------------------------------------------------------------------------------- 1 | This is a demo project for Elixir in Action. 2 | 3 | To be able to compile and run it, you'll need Elixir 1.0.0 and Erlang 17.0. A `git` client should also be installed and available somewhere in the execution path. 4 | 5 | Prior to the first build, you need to call `mix deps.get`. After that, you can build and start the interactive shell by running `iex -S mix` from this project root. -------------------------------------------------------------------------------- /code_samples/ch10/todo_cache_expiry/lib/todo/cache.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Cache do 2 | def start_link() do 3 | IO.puts("Starting to-do cache.") 4 | DynamicSupervisor.start_link(name: __MODULE__, strategy: :one_for_one) 5 | end 6 | 7 | def child_spec(_arg) do 8 | %{ 9 | id: __MODULE__, 10 | start: {__MODULE__, :start_link, []}, 11 | type: :supervisor 12 | } 13 | end 14 | 15 | def server_process(todo_list_name) do 16 | case start_child(todo_list_name) do 17 | {:ok, pid} -> pid 18 | {:error, {:already_started, pid}} -> pid 19 | end 20 | end 21 | 22 | defp start_child(todo_list_name) do 23 | DynamicSupervisor.start_child(__MODULE__, {Todo.Server, todo_list_name}) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_cache_expiry/lib/todo/database.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Database do 2 | @pool_size 3 3 | @db_folder "./persist" 4 | 5 | def start_link do 6 | IO.puts("Starting database server.") 7 | File.mkdir_p!(@db_folder) 8 | 9 | children = Enum.map(1..@pool_size, &worker_spec/1) 10 | Supervisor.start_link(children, strategy: :one_for_one) 11 | end 12 | 13 | defp worker_spec(worker_id) do 14 | default_worker_spec = {Todo.DatabaseWorker, {@db_folder, worker_id}} 15 | Supervisor.child_spec(default_worker_spec, id: worker_id) 16 | end 17 | 18 | def child_spec(_) do 19 | %{ 20 | id: __MODULE__, 21 | start: {__MODULE__, :start_link, []}, 22 | type: :supervisor 23 | } 24 | end 25 | 26 | def store(key, data) do 27 | key 28 | |> choose_worker() 29 | |> Todo.DatabaseWorker.store(key, data) 30 | end 31 | 32 | def get(key) do 33 | key 34 | |> choose_worker() 35 | |> Todo.DatabaseWorker.get(key) 36 | end 37 | 38 | defp choose_worker(key) do 39 | :erlang.phash2(key, @pool_size) + 1 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_cache_expiry/lib/todo/database_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.DatabaseWorker do 2 | use GenServer 3 | 4 | def start_link({db_folder, worker_id}) do 5 | GenServer.start_link( 6 | __MODULE__, 7 | db_folder, 8 | name: via_tuple(worker_id) 9 | ) 10 | end 11 | 12 | def store(worker_id, key, data) do 13 | GenServer.cast(via_tuple(worker_id), {:store, key, data}) 14 | end 15 | 16 | def get(worker_id, key) do 17 | GenServer.call(via_tuple(worker_id), {:get, key}) 18 | end 19 | 20 | defp via_tuple(worker_id) do 21 | Todo.ProcessRegistry.via_tuple({__MODULE__, worker_id}) 22 | end 23 | 24 | @impl GenServer 25 | def init(db_folder) do 26 | IO.puts("Starting database worker.") 27 | {:ok, db_folder} 28 | end 29 | 30 | @impl GenServer 31 | def handle_cast({:store, key, data}, db_folder) do 32 | db_folder 33 | |> file_name(key) 34 | |> File.write!(:erlang.term_to_binary(data)) 35 | 36 | {:noreply, db_folder} 37 | end 38 | 39 | @impl GenServer 40 | def handle_call({:get, key}, _, db_folder) do 41 | data = 42 | case File.read(file_name(db_folder, key)) do 43 | {:ok, contents} -> :erlang.binary_to_term(contents) 44 | _ -> nil 45 | end 46 | 47 | {:reply, data, db_folder} 48 | end 49 | 50 | defp file_name(db_folder, key) do 51 | Path.join(db_folder, to_string(key)) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_cache_expiry/lib/todo/list.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.List do 2 | defstruct next_id: 1, entries: %{} 3 | 4 | def new(entries \\ []) do 5 | Enum.reduce( 6 | entries, 7 | %Todo.List{}, 8 | &add_entry(&2, &1) 9 | ) 10 | end 11 | 12 | def size(todo_list) do 13 | map_size(todo_list.entries) 14 | end 15 | 16 | def add_entry(todo_list, entry) do 17 | entry = Map.put(entry, :id, todo_list.next_id) 18 | new_entries = Map.put(todo_list.entries, todo_list.next_id, entry) 19 | 20 | %Todo.List{todo_list | entries: new_entries, next_id: todo_list.next_id + 1} 21 | end 22 | 23 | def entries(todo_list, date) do 24 | todo_list.entries 25 | |> Map.values() 26 | |> Enum.filter(fn entry -> entry.date == date end) 27 | end 28 | 29 | def update_entry(todo_list, entry_id, updater_fun) do 30 | case Map.fetch(todo_list.entries, entry_id) do 31 | :error -> 32 | todo_list 33 | 34 | {:ok, old_entry} -> 35 | new_entry = updater_fun.(old_entry) 36 | new_entries = Map.put(todo_list.entries, new_entry.id, new_entry) 37 | %Todo.List{todo_list | entries: new_entries} 38 | end 39 | end 40 | 41 | def delete_entry(todo_list, entry_id) do 42 | %Todo.List{todo_list | entries: Map.delete(todo_list.entries, entry_id)} 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_cache_expiry/lib/todo/metrics.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Metrics do 2 | use Task 3 | 4 | def start_link(_arg) do 5 | Task.start_link(&loop/0) 6 | end 7 | 8 | defp loop() do 9 | Process.sleep(:timer.seconds(10)) 10 | IO.inspect(collect_metrics()) 11 | loop() 12 | end 13 | 14 | defp collect_metrics() do 15 | [ 16 | memory_usage: :erlang.memory(:total), 17 | process_count: :erlang.system_info(:process_count) 18 | ] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_cache_expiry/lib/todo/process_registry.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.ProcessRegistry do 2 | def start_link do 3 | Registry.start_link(keys: :unique, name: __MODULE__) 4 | end 5 | 6 | def via_tuple(key) do 7 | {:via, Registry, {__MODULE__, key}} 8 | end 9 | 10 | def child_spec(_) do 11 | Supervisor.child_spec( 12 | Registry, 13 | id: __MODULE__, 14 | start: {__MODULE__, :start_link, []} 15 | ) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_cache_expiry/lib/todo/system.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.System do 2 | def start_link do 3 | Supervisor.start_link( 4 | [ 5 | Todo.Metrics, 6 | Todo.ProcessRegistry, 7 | Todo.Database, 8 | Todo.Cache 9 | ], 10 | strategy: :one_for_one 11 | ) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_cache_expiry/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :todo, 7 | version: "0.1.0", 8 | elixir: "~> 1.15", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | def application do 15 | [ 16 | extra_applications: [:logger] 17 | ] 18 | end 19 | 20 | defp deps do 21 | [] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_cache_expiry/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [], [], "hexpm"}, 3 | } 4 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_cache_expiry/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | File.rm_rf!("./persist") 2 | File.mkdir_p!("./persist") 3 | ExUnit.start() 4 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_cache_expiry/test/todo/cache_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.CacheTest do 2 | use ExUnit.Case 3 | 4 | setup_all do 5 | {:ok, todo_system_pid} = Todo.System.start_link() 6 | {:ok, todo_system_pid: todo_system_pid} 7 | end 8 | 9 | test "server_process" do 10 | bob_pid = Todo.Cache.server_process("bob") 11 | 12 | assert bob_pid != Todo.Cache.server_process("alice") 13 | assert bob_pid == Todo.Cache.server_process("bob") 14 | end 15 | 16 | test "to-do operations" do 17 | jane = Todo.Cache.server_process("jane") 18 | Todo.Server.add_entry(jane, %{date: ~D[2018-12-19], title: "Dentist"}) 19 | entries = Todo.Server.entries(jane, ~D[2018-12-19]) 20 | 21 | assert [%{date: ~D[2018-12-19], title: "Dentist"}] = entries 22 | end 23 | 24 | test "persistence" do 25 | john = Todo.Cache.server_process("john") 26 | Todo.Server.add_entry(john, %{date: ~D[2018-12-20], title: "Shopping"}) 27 | assert 1 == length(Todo.Server.entries(john, ~D[2018-12-20])) 28 | 29 | Process.exit(john, :kill) 30 | 31 | entries = 32 | "john" 33 | |> Todo.Cache.server_process() 34 | |> Todo.Server.entries(~D[2018-12-20]) 35 | 36 | assert [%{date: ~D[2018-12-20], title: "Shopping"}] = entries 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_metrics/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_metrics/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | /persist/ 6 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_metrics/README.md: -------------------------------------------------------------------------------- 1 | This is a demo project for Elixir in Action. 2 | 3 | To be able to compile and run it, you'll need Elixir 1.0.0 and Erlang 17.0. A `git` client should also be installed and available somewhere in the execution path. 4 | 5 | Prior to the first build, you need to call `mix deps.get`. After that, you can build and start the interactive shell by running `iex -S mix` from this project root. -------------------------------------------------------------------------------- /code_samples/ch10/todo_metrics/lib/todo/cache.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Cache do 2 | def start_link() do 3 | IO.puts("Starting to-do cache.") 4 | DynamicSupervisor.start_link(name: __MODULE__, strategy: :one_for_one) 5 | end 6 | 7 | def child_spec(_arg) do 8 | %{ 9 | id: __MODULE__, 10 | start: {__MODULE__, :start_link, []}, 11 | type: :supervisor 12 | } 13 | end 14 | 15 | def server_process(todo_list_name) do 16 | case start_child(todo_list_name) do 17 | {:ok, pid} -> pid 18 | {:error, {:already_started, pid}} -> pid 19 | end 20 | end 21 | 22 | defp start_child(todo_list_name) do 23 | DynamicSupervisor.start_child(__MODULE__, {Todo.Server, todo_list_name}) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_metrics/lib/todo/database.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Database do 2 | @pool_size 3 3 | @db_folder "./persist" 4 | 5 | def start_link do 6 | IO.puts("Starting database server.") 7 | File.mkdir_p!(@db_folder) 8 | 9 | children = Enum.map(1..@pool_size, &worker_spec/1) 10 | Supervisor.start_link(children, strategy: :one_for_one) 11 | end 12 | 13 | defp worker_spec(worker_id) do 14 | default_worker_spec = {Todo.DatabaseWorker, {@db_folder, worker_id}} 15 | Supervisor.child_spec(default_worker_spec, id: worker_id) 16 | end 17 | 18 | def child_spec(_) do 19 | %{ 20 | id: __MODULE__, 21 | start: {__MODULE__, :start_link, []}, 22 | type: :supervisor 23 | } 24 | end 25 | 26 | def store(key, data) do 27 | key 28 | |> choose_worker() 29 | |> Todo.DatabaseWorker.store(key, data) 30 | end 31 | 32 | def get(key) do 33 | key 34 | |> choose_worker() 35 | |> Todo.DatabaseWorker.get(key) 36 | end 37 | 38 | defp choose_worker(key) do 39 | :erlang.phash2(key, @pool_size) + 1 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_metrics/lib/todo/database_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.DatabaseWorker do 2 | use GenServer 3 | 4 | def start_link({db_folder, worker_id}) do 5 | GenServer.start_link( 6 | __MODULE__, 7 | db_folder, 8 | name: via_tuple(worker_id) 9 | ) 10 | end 11 | 12 | def store(worker_id, key, data) do 13 | GenServer.cast(via_tuple(worker_id), {:store, key, data}) 14 | end 15 | 16 | def get(worker_id, key) do 17 | GenServer.call(via_tuple(worker_id), {:get, key}) 18 | end 19 | 20 | defp via_tuple(worker_id) do 21 | Todo.ProcessRegistry.via_tuple({__MODULE__, worker_id}) 22 | end 23 | 24 | @impl GenServer 25 | def init(db_folder) do 26 | IO.puts("Starting database worker.") 27 | {:ok, db_folder} 28 | end 29 | 30 | @impl GenServer 31 | def handle_cast({:store, key, data}, db_folder) do 32 | db_folder 33 | |> file_name(key) 34 | |> File.write!(:erlang.term_to_binary(data)) 35 | 36 | {:noreply, db_folder} 37 | end 38 | 39 | @impl GenServer 40 | def handle_call({:get, key}, _, db_folder) do 41 | data = 42 | case File.read(file_name(db_folder, key)) do 43 | {:ok, contents} -> :erlang.binary_to_term(contents) 44 | _ -> nil 45 | end 46 | 47 | {:reply, data, db_folder} 48 | end 49 | 50 | defp file_name(db_folder, key) do 51 | Path.join(db_folder, to_string(key)) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_metrics/lib/todo/list.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.List do 2 | defstruct next_id: 1, entries: %{} 3 | 4 | def new(entries \\ []) do 5 | Enum.reduce( 6 | entries, 7 | %Todo.List{}, 8 | &add_entry(&2, &1) 9 | ) 10 | end 11 | 12 | def size(todo_list) do 13 | map_size(todo_list.entries) 14 | end 15 | 16 | def add_entry(todo_list, entry) do 17 | entry = Map.put(entry, :id, todo_list.next_id) 18 | new_entries = Map.put(todo_list.entries, todo_list.next_id, entry) 19 | 20 | %Todo.List{todo_list | entries: new_entries, next_id: todo_list.next_id + 1} 21 | end 22 | 23 | def entries(todo_list, date) do 24 | todo_list.entries 25 | |> Map.values() 26 | |> Enum.filter(fn entry -> entry.date == date end) 27 | end 28 | 29 | def update_entry(todo_list, entry_id, updater_fun) do 30 | case Map.fetch(todo_list.entries, entry_id) do 31 | :error -> 32 | todo_list 33 | 34 | {:ok, old_entry} -> 35 | new_entry = updater_fun.(old_entry) 36 | new_entries = Map.put(todo_list.entries, new_entry.id, new_entry) 37 | %Todo.List{todo_list | entries: new_entries} 38 | end 39 | end 40 | 41 | def delete_entry(todo_list, entry_id) do 42 | %Todo.List{todo_list | entries: Map.delete(todo_list.entries, entry_id)} 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_metrics/lib/todo/metrics.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Metrics do 2 | use Task 3 | 4 | def start_link(_arg) do 5 | Task.start_link(&loop/0) 6 | end 7 | 8 | defp loop() do 9 | Process.sleep(:timer.seconds(10)) 10 | IO.inspect(collect_metrics()) 11 | loop() 12 | end 13 | 14 | defp collect_metrics() do 15 | [ 16 | memory_usage: :erlang.memory(:total), 17 | process_count: :erlang.system_info(:process_count) 18 | ] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_metrics/lib/todo/process_registry.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.ProcessRegistry do 2 | def start_link do 3 | Registry.start_link(keys: :unique, name: __MODULE__) 4 | end 5 | 6 | def via_tuple(key) do 7 | {:via, Registry, {__MODULE__, key}} 8 | end 9 | 10 | def child_spec(_) do 11 | Supervisor.child_spec( 12 | Registry, 13 | id: __MODULE__, 14 | start: {__MODULE__, :start_link, []} 15 | ) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_metrics/lib/todo/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Server do 2 | use GenServer, restart: :temporary 3 | 4 | def start_link(name) do 5 | GenServer.start_link(Todo.Server, name, name: via_tuple(name)) 6 | end 7 | 8 | def add_entry(todo_server, new_entry) do 9 | GenServer.cast(todo_server, {:add_entry, new_entry}) 10 | end 11 | 12 | def entries(todo_server, date) do 13 | GenServer.call(todo_server, {:entries, date}) 14 | end 15 | 16 | defp via_tuple(name) do 17 | Todo.ProcessRegistry.via_tuple({__MODULE__, name}) 18 | end 19 | 20 | @impl GenServer 21 | def init(name) do 22 | IO.puts("Starting to-do server for #{name}.") 23 | {:ok, {name, nil}, {:continue, :init}} 24 | end 25 | 26 | @impl GenServer 27 | def handle_continue(:init, {name, nil}) do 28 | todo_list = Todo.Database.get(name) || Todo.List.new() 29 | {:noreply, {name, todo_list}} 30 | end 31 | 32 | @impl GenServer 33 | def handle_cast({:add_entry, new_entry}, {name, todo_list}) do 34 | new_list = Todo.List.add_entry(todo_list, new_entry) 35 | Todo.Database.store(name, new_list) 36 | {:noreply, {name, new_list}} 37 | end 38 | 39 | @impl GenServer 40 | def handle_call({:entries, date}, _, {name, todo_list}) do 41 | { 42 | :reply, 43 | Todo.List.entries(todo_list, date), 44 | {name, todo_list} 45 | } 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_metrics/lib/todo/system.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.System do 2 | def start_link do 3 | Supervisor.start_link( 4 | [ 5 | Todo.Metrics, 6 | Todo.ProcessRegistry, 7 | Todo.Database, 8 | Todo.Cache 9 | ], 10 | strategy: :one_for_one 11 | ) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_metrics/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :todo, 7 | version: "0.1.0", 8 | elixir: "~> 1.15", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | def application do 15 | [ 16 | extra_applications: [:logger] 17 | ] 18 | end 19 | 20 | defp deps do 21 | [] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_metrics/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [], [], "hexpm"}, 3 | } 4 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_metrics/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | File.rm_rf!("./persist") 2 | File.mkdir_p!("./persist") 3 | ExUnit.start() 4 | -------------------------------------------------------------------------------- /code_samples/ch10/todo_metrics/test/todo/cache_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.CacheTest do 2 | use ExUnit.Case 3 | 4 | setup_all do 5 | {:ok, todo_system_pid} = Todo.System.start_link() 6 | {:ok, todo_system_pid: todo_system_pid} 7 | end 8 | 9 | test "server_process" do 10 | bob_pid = Todo.Cache.server_process("bob") 11 | 12 | assert bob_pid != Todo.Cache.server_process("alice") 13 | assert bob_pid == Todo.Cache.server_process("bob") 14 | end 15 | 16 | test "to-do operations" do 17 | jane = Todo.Cache.server_process("jane") 18 | Todo.Server.add_entry(jane, %{date: ~D[2018-12-19], title: "Dentist"}) 19 | entries = Todo.Server.entries(jane, ~D[2018-12-19]) 20 | 21 | assert [%{date: ~D[2018-12-19], title: "Dentist"}] = entries 22 | end 23 | 24 | test "persistence" do 25 | john = Todo.Cache.server_process("john") 26 | Todo.Server.add_entry(john, %{date: ~D[2018-12-20], title: "Shopping"}) 27 | assert 1 == length(Todo.Server.entries(john, ~D[2018-12-20])) 28 | 29 | Process.exit(john, :kill) 30 | 31 | entries = 32 | "john" 33 | |> Todo.Cache.server_process() 34 | |> Todo.Server.entries(~D[2018-12-20]) 35 | 36 | assert [%{date: ~D[2018-12-20], title: "Shopping"}] = entries 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_app/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_app/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | /persist/ 6 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_app/README.md: -------------------------------------------------------------------------------- 1 | This is a demo project for Elixir in Action. 2 | 3 | To be able to compile and run it, you'll need Elixir 1.0.0 and Erlang 17.0. A `git` client should also be installed and available somewhere in the execution path. 4 | 5 | Prior to the first build, you need to call `mix deps.get`. After that, you can build and start the interactive shell by running `iex -S mix` from this project root. -------------------------------------------------------------------------------- /code_samples/ch11/todo_app/lib/todo/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Application do 2 | use Application 3 | 4 | @impl Application 5 | def start(_, _) do 6 | Todo.System.start_link() 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_app/lib/todo/cache.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Cache do 2 | def start_link() do 3 | IO.puts("Starting to-do cache.") 4 | DynamicSupervisor.start_link(name: __MODULE__, strategy: :one_for_one) 5 | end 6 | 7 | def child_spec(_arg) do 8 | %{ 9 | id: __MODULE__, 10 | start: {__MODULE__, :start_link, []}, 11 | type: :supervisor 12 | } 13 | end 14 | 15 | def server_process(todo_list_name) do 16 | case start_child(todo_list_name) do 17 | {:ok, pid} -> pid 18 | {:error, {:already_started, pid}} -> pid 19 | end 20 | end 21 | 22 | defp start_child(todo_list_name) do 23 | DynamicSupervisor.start_child(__MODULE__, {Todo.Server, todo_list_name}) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_app/lib/todo/database.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Database do 2 | @pool_size 3 3 | @db_folder "./persist" 4 | 5 | def start_link do 6 | IO.puts("Starting database server.") 7 | File.mkdir_p!(@db_folder) 8 | 9 | children = Enum.map(1..@pool_size, &worker_spec/1) 10 | Supervisor.start_link(children, strategy: :one_for_one) 11 | end 12 | 13 | defp worker_spec(worker_id) do 14 | default_worker_spec = {Todo.DatabaseWorker, {@db_folder, worker_id}} 15 | Supervisor.child_spec(default_worker_spec, id: worker_id) 16 | end 17 | 18 | def child_spec(_) do 19 | %{ 20 | id: __MODULE__, 21 | start: {__MODULE__, :start_link, []}, 22 | type: :supervisor 23 | } 24 | end 25 | 26 | def store(key, data) do 27 | key 28 | |> choose_worker() 29 | |> Todo.DatabaseWorker.store(key, data) 30 | end 31 | 32 | def get(key) do 33 | key 34 | |> choose_worker() 35 | |> Todo.DatabaseWorker.get(key) 36 | end 37 | 38 | defp choose_worker(key) do 39 | :erlang.phash2(key, @pool_size) + 1 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_app/lib/todo/database_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.DatabaseWorker do 2 | use GenServer 3 | 4 | def start_link({db_folder, worker_id}) do 5 | GenServer.start_link( 6 | __MODULE__, 7 | db_folder, 8 | name: via_tuple(worker_id) 9 | ) 10 | end 11 | 12 | def store(worker_id, key, data) do 13 | GenServer.cast(via_tuple(worker_id), {:store, key, data}) 14 | end 15 | 16 | def get(worker_id, key) do 17 | GenServer.call(via_tuple(worker_id), {:get, key}) 18 | end 19 | 20 | defp via_tuple(worker_id) do 21 | Todo.ProcessRegistry.via_tuple({__MODULE__, worker_id}) 22 | end 23 | 24 | @impl GenServer 25 | def init(db_folder) do 26 | IO.puts("Starting database worker.") 27 | {:ok, db_folder} 28 | end 29 | 30 | @impl GenServer 31 | def handle_cast({:store, key, data}, db_folder) do 32 | db_folder 33 | |> file_name(key) 34 | |> File.write!(:erlang.term_to_binary(data)) 35 | 36 | {:noreply, db_folder} 37 | end 38 | 39 | @impl GenServer 40 | def handle_call({:get, key}, _, db_folder) do 41 | data = 42 | case File.read(file_name(db_folder, key)) do 43 | {:ok, contents} -> :erlang.binary_to_term(contents) 44 | _ -> nil 45 | end 46 | 47 | {:reply, data, db_folder} 48 | end 49 | 50 | defp file_name(db_folder, key) do 51 | Path.join(db_folder, to_string(key)) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_app/lib/todo/list.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.List do 2 | defstruct next_id: 1, entries: %{} 3 | 4 | def new(entries \\ []) do 5 | Enum.reduce( 6 | entries, 7 | %Todo.List{}, 8 | &add_entry(&2, &1) 9 | ) 10 | end 11 | 12 | def size(todo_list) do 13 | map_size(todo_list.entries) 14 | end 15 | 16 | def add_entry(todo_list, entry) do 17 | entry = Map.put(entry, :id, todo_list.next_id) 18 | new_entries = Map.put(todo_list.entries, todo_list.next_id, entry) 19 | 20 | %Todo.List{todo_list | entries: new_entries, next_id: todo_list.next_id + 1} 21 | end 22 | 23 | def entries(todo_list, date) do 24 | todo_list.entries 25 | |> Map.values() 26 | |> Enum.filter(fn entry -> entry.date == date end) 27 | end 28 | 29 | def update_entry(todo_list, entry_id, updater_fun) do 30 | case Map.fetch(todo_list.entries, entry_id) do 31 | :error -> 32 | todo_list 33 | 34 | {:ok, old_entry} -> 35 | new_entry = updater_fun.(old_entry) 36 | new_entries = Map.put(todo_list.entries, new_entry.id, new_entry) 37 | %Todo.List{todo_list | entries: new_entries} 38 | end 39 | end 40 | 41 | def delete_entry(todo_list, entry_id) do 42 | %Todo.List{todo_list | entries: Map.delete(todo_list.entries, entry_id)} 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_app/lib/todo/metrics.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Metrics do 2 | use Task 3 | 4 | def start_link(_arg) do 5 | Task.start_link(&loop/0) 6 | end 7 | 8 | defp loop() do 9 | Process.sleep(:timer.seconds(10)) 10 | IO.inspect(collect_metrics()) 11 | loop() 12 | end 13 | 14 | defp collect_metrics() do 15 | [ 16 | memory_usage: :erlang.memory(:total), 17 | process_count: :erlang.system_info(:process_count) 18 | ] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_app/lib/todo/process_registry.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.ProcessRegistry do 2 | def start_link do 3 | Registry.start_link(keys: :unique, name: __MODULE__) 4 | end 5 | 6 | def via_tuple(key) do 7 | {:via, Registry, {__MODULE__, key}} 8 | end 9 | 10 | def child_spec(_) do 11 | Supervisor.child_spec( 12 | Registry, 13 | id: __MODULE__, 14 | start: {__MODULE__, :start_link, []} 15 | ) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_app/lib/todo/system.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.System do 2 | def start_link do 3 | Supervisor.start_link( 4 | [ 5 | Todo.Metrics, 6 | Todo.ProcessRegistry, 7 | Todo.Database, 8 | Todo.Cache 9 | ], 10 | strategy: :one_for_one 11 | ) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_app/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :todo, 7 | version: "0.1.0", 8 | elixir: "~> 1.15", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | def application do 15 | [ 16 | extra_applications: [:logger], 17 | mod: {Todo.Application, []} 18 | ] 19 | end 20 | 21 | defp deps do 22 | [] 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_app/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [], [], "hexpm"}, 3 | } 4 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_app/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | File.rm_rf!("./persist") 2 | File.mkdir_p!("./persist") 3 | ExUnit.start() 4 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_app/test/todo/cache_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.CacheTest do 2 | use ExUnit.Case 3 | 4 | test "server_process" do 5 | bob_pid = Todo.Cache.server_process("bob") 6 | 7 | assert bob_pid != Todo.Cache.server_process("alice") 8 | assert bob_pid == Todo.Cache.server_process("bob") 9 | end 10 | 11 | test "to-do operations" do 12 | jane = Todo.Cache.server_process("jane") 13 | Todo.Server.add_entry(jane, %{date: ~D[2018-12-19], title: "Dentist"}) 14 | entries = Todo.Server.entries(jane, ~D[2018-12-19]) 15 | 16 | assert [%{date: ~D[2018-12-19], title: "Dentist"}] = entries 17 | end 18 | 19 | test "persistence" do 20 | john = Todo.Cache.server_process("john") 21 | Todo.Server.add_entry(john, %{date: ~D[2018-12-20], title: "Shopping"}) 22 | assert 1 == length(Todo.Server.entries(john, ~D[2018-12-20])) 23 | 24 | Process.exit(john, :kill) 25 | 26 | entries = 27 | "john" 28 | |> Todo.Cache.server_process() 29 | |> Todo.Server.entries(~D[2018-12-20]) 30 | 31 | assert [%{date: ~D[2018-12-20], title: "Shopping"}] = entries 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_env/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_env/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | /persist/ 6 | /test_persist/ 7 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_env/README.md: -------------------------------------------------------------------------------- 1 | This is a demo project for Elixir in Action. 2 | 3 | To be able to compile and run it, you'll need Elixir 1.0.0 and Erlang 17.0. A `git` client should also be installed and available somewhere in the execution path. 4 | 5 | Prior to the first build, you need to call `mix deps.get`. After that, you can build and start the interactive shell by running `iex -S mix` from this project root. -------------------------------------------------------------------------------- /code_samples/ch11/todo_env/config/runtime.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # Using a different http port in test to allow running tests while the iex shell session is running. 4 | http_port = 5 | if config_env() != :test, 6 | do: System.get_env("TODO_HTTP_PORT", "5454"), 7 | else: System.get_env("TODO_TEST_HTTP_PORT", "5455") 8 | 9 | config :todo, http_port: String.to_integer(http_port) 10 | 11 | # Using a different db_folder in test to avoid polluting the dev db. 12 | db_folder = 13 | if config_env() != :test, 14 | do: System.get_env("TODO_DB_FOLDER", "./persist"), 15 | else: System.get_env("TODO_TEST_DB_FOLDER", "./test_persist") 16 | 17 | config :todo, :database, db_folder: db_folder 18 | 19 | # Using a shorter to-do server expiry in local dev. 20 | todo_server_expiry = 21 | if config_env() != :dev, 22 | do: System.get_env("TODO_SERVER_EXPIRY", "60"), 23 | else: System.get_env("TODO_SERVER_EXPIRY", "10") 24 | 25 | config :todo, todo_server_expiry: :timer.seconds(String.to_integer(todo_server_expiry)) 26 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_env/lib/todo/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Application do 2 | use Application 3 | 4 | @impl Application 5 | def start(_, _) do 6 | Todo.System.start_link() 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_env/lib/todo/cache.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Cache do 2 | def start_link() do 3 | IO.puts("Starting to-do cache.") 4 | DynamicSupervisor.start_link(name: __MODULE__, strategy: :one_for_one) 5 | end 6 | 7 | def child_spec(_arg) do 8 | %{ 9 | id: __MODULE__, 10 | start: {__MODULE__, :start_link, []}, 11 | type: :supervisor 12 | } 13 | end 14 | 15 | def server_process(todo_list_name) do 16 | case start_child(todo_list_name) do 17 | {:ok, pid} -> pid 18 | {:error, {:already_started, pid}} -> pid 19 | end 20 | end 21 | 22 | defp start_child(todo_list_name) do 23 | DynamicSupervisor.start_child(__MODULE__, {Todo.Server, todo_list_name}) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_env/lib/todo/database.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Database do 2 | def child_spec(_) do 3 | db_settings = Application.fetch_env!(:todo, :database) 4 | db_folder = Keyword.fetch!(db_settings, :db_folder) 5 | 6 | File.mkdir_p!(db_folder) 7 | 8 | :poolboy.child_spec( 9 | __MODULE__, 10 | [ 11 | name: {:local, __MODULE__}, 12 | worker_module: Todo.DatabaseWorker, 13 | size: 3 14 | ], 15 | [db_folder] 16 | ) 17 | end 18 | 19 | def store(key, data) do 20 | :poolboy.transaction(__MODULE__, fn worker_pid -> 21 | Todo.DatabaseWorker.store(worker_pid, key, data) 22 | end) 23 | end 24 | 25 | def get(key) do 26 | :poolboy.transaction(__MODULE__, fn worker_pid -> Todo.DatabaseWorker.get(worker_pid, key) end) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_env/lib/todo/database_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.DatabaseWorker do 2 | use GenServer 3 | 4 | def start_link(db_folder) do 5 | GenServer.start_link(__MODULE__, db_folder) 6 | end 7 | 8 | def store(pid, key, data) do 9 | GenServer.cast(pid, {:store, key, data}) 10 | end 11 | 12 | def get(pid, key) do 13 | GenServer.call(pid, {:get, key}) 14 | end 15 | 16 | @impl GenServer 17 | def init(db_folder) do 18 | IO.puts("Starting database worker.") 19 | {:ok, db_folder} 20 | end 21 | 22 | @impl GenServer 23 | def handle_cast({:store, key, data}, db_folder) do 24 | db_folder 25 | |> file_name(key) 26 | |> File.write!(:erlang.term_to_binary(data)) 27 | 28 | {:noreply, db_folder} 29 | end 30 | 31 | @impl GenServer 32 | def handle_call({:get, key}, _, db_folder) do 33 | data = 34 | case File.read(file_name(db_folder, key)) do 35 | {:ok, contents} -> :erlang.binary_to_term(contents) 36 | _ -> nil 37 | end 38 | 39 | {:reply, data, db_folder} 40 | end 41 | 42 | defp file_name(db_folder, key) do 43 | Path.join(db_folder, to_string(key)) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_env/lib/todo/list.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.List do 2 | defstruct next_id: 1, entries: %{} 3 | 4 | def new(entries \\ []) do 5 | Enum.reduce( 6 | entries, 7 | %Todo.List{}, 8 | &add_entry(&2, &1) 9 | ) 10 | end 11 | 12 | def size(todo_list) do 13 | map_size(todo_list.entries) 14 | end 15 | 16 | def add_entry(todo_list, entry) do 17 | entry = Map.put(entry, :id, todo_list.next_id) 18 | new_entries = Map.put(todo_list.entries, todo_list.next_id, entry) 19 | 20 | %Todo.List{todo_list | entries: new_entries, next_id: todo_list.next_id + 1} 21 | end 22 | 23 | def entries(todo_list, date) do 24 | todo_list.entries 25 | |> Map.values() 26 | |> Enum.filter(fn entry -> entry.date == date end) 27 | end 28 | 29 | def update_entry(todo_list, entry_id, updater_fun) do 30 | case Map.fetch(todo_list.entries, entry_id) do 31 | :error -> 32 | todo_list 33 | 34 | {:ok, old_entry} -> 35 | new_entry = updater_fun.(old_entry) 36 | new_entries = Map.put(todo_list.entries, new_entry.id, new_entry) 37 | %Todo.List{todo_list | entries: new_entries} 38 | end 39 | end 40 | 41 | def delete_entry(todo_list, entry_id) do 42 | %Todo.List{todo_list | entries: Map.delete(todo_list.entries, entry_id)} 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_env/lib/todo/metrics.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Metrics do 2 | use Task 3 | 4 | def start_link(_arg) do 5 | Task.start_link(&loop/0) 6 | end 7 | 8 | defp loop() do 9 | Process.sleep(:timer.seconds(10)) 10 | IO.inspect(collect_metrics()) 11 | loop() 12 | end 13 | 14 | defp collect_metrics() do 15 | [ 16 | memory_usage: :erlang.memory(:total), 17 | process_count: :erlang.system_info(:process_count) 18 | ] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_env/lib/todo/process_registry.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.ProcessRegistry do 2 | def start_link do 3 | Registry.start_link(keys: :unique, name: __MODULE__) 4 | end 5 | 6 | def via_tuple(key) do 7 | {:via, Registry, {__MODULE__, key}} 8 | end 9 | 10 | def child_spec(_) do 11 | Supervisor.child_spec( 12 | Registry, 13 | id: __MODULE__, 14 | start: {__MODULE__, :start_link, []} 15 | ) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_env/lib/todo/system.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.System do 2 | def start_link do 3 | Supervisor.start_link( 4 | [ 5 | Todo.Metrics, 6 | Todo.ProcessRegistry, 7 | Todo.Database, 8 | Todo.Cache, 9 | Todo.Web 10 | ], 11 | strategy: :one_for_one 12 | ) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_env/lib/todo/web.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Web do 2 | use Plug.Router 3 | 4 | plug(:match) 5 | plug(:dispatch) 6 | 7 | def child_spec(_arg) do 8 | Plug.Cowboy.child_spec( 9 | scheme: :http, 10 | options: [port: Application.fetch_env!(:todo, :http_port)], 11 | plug: __MODULE__ 12 | ) 13 | end 14 | 15 | # curl 'http://localhost:5454/entries?list=bob&date=2018-12-19' 16 | get "/entries" do 17 | conn = Plug.Conn.fetch_query_params(conn) 18 | list_name = Map.fetch!(conn.params, "list") 19 | date = Date.from_iso8601!(Map.fetch!(conn.params, "date")) 20 | 21 | entries = 22 | list_name 23 | |> Todo.Cache.server_process() 24 | |> Todo.Server.entries(date) 25 | 26 | formatted_entries = 27 | entries 28 | |> Enum.map(&"#{&1.date} #{&1.title}") 29 | |> Enum.join("\n") 30 | 31 | conn 32 | |> Plug.Conn.put_resp_content_type("text/plain") 33 | |> Plug.Conn.send_resp(200, formatted_entries) 34 | end 35 | 36 | # curl -d '' 'http://localhost:5454/add_entry?list=bob&date=2018-12-19&title=Dentist' 37 | post "/add_entry" do 38 | conn = Plug.Conn.fetch_query_params(conn) 39 | list_name = Map.fetch!(conn.params, "list") 40 | title = Map.fetch!(conn.params, "title") 41 | date = Date.from_iso8601!(Map.fetch!(conn.params, "date")) 42 | 43 | list_name 44 | |> Todo.Cache.server_process() 45 | |> Todo.Server.add_entry(%{title: title, date: date}) 46 | 47 | conn 48 | |> Plug.Conn.put_resp_content_type("text/plain") 49 | |> Plug.Conn.send_resp(200, "OK") 50 | end 51 | 52 | match _ do 53 | Plug.Conn.send_resp(conn, 404, "not found") 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_env/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :todo, 7 | version: "0.1.0", 8 | elixir: "~> 1.15", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | def application do 15 | [ 16 | extra_applications: [:logger], 17 | mod: {Todo.Application, []} 18 | ] 19 | end 20 | 21 | defp deps do 22 | [ 23 | {:poolboy, "~> 1.5"}, 24 | {:plug_cowboy, "~> 2.6"} 25 | ] 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_env/test/http_server_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HttpServerTest do 2 | use ExUnit.Case, async: false 3 | use Plug.Test 4 | 5 | test "no entries" do 6 | assert get("/entries?list=test_1&date=2018-12-19").status == 200 7 | assert get("/entries?list=test_1&date=2018-12-19").resp_body == "" 8 | end 9 | 10 | test "adding an entry" do 11 | resp = post("/add_entry?list=test_2&date=2018-12-19&title=Dentist") 12 | 13 | assert resp.status == 200 14 | assert resp.resp_body == "OK" 15 | assert get("/entries?list=test_2&date=2018-12-19").resp_body == "2018-12-19 Dentist" 16 | end 17 | 18 | defp get(path) do 19 | Todo.Web.call(conn(:get, path), Todo.Web.init([])) 20 | end 21 | 22 | defp post(path) do 23 | Todo.Web.call(conn(:post, path), Todo.Web.init([])) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_env/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | db_settings = Application.fetch_env!(:todo, :database) 2 | db_folder = Keyword.fetch!(db_settings, :db_folder) 3 | File.rm_rf!(db_folder) 4 | File.mkdir_p!(db_folder) 5 | ExUnit.start() 6 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_env/test/todo/cache_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.CacheTest do 2 | use ExUnit.Case 3 | 4 | test "server_process" do 5 | bob_pid = Todo.Cache.server_process("bob") 6 | 7 | assert bob_pid != Todo.Cache.server_process("alice") 8 | assert bob_pid == Todo.Cache.server_process("bob") 9 | end 10 | 11 | test "to-do operations" do 12 | jane = Todo.Cache.server_process("jane") 13 | Todo.Server.add_entry(jane, %{date: ~D[2018-12-19], title: "Dentist"}) 14 | entries = Todo.Server.entries(jane, ~D[2018-12-19]) 15 | 16 | assert [%{date: ~D[2018-12-19], title: "Dentist"}] = entries 17 | end 18 | 19 | test "persistence" do 20 | john = Todo.Cache.server_process("john") 21 | Todo.Server.add_entry(john, %{date: ~D[2018-12-20], title: "Shopping"}) 22 | assert 1 == length(Todo.Server.entries(john, ~D[2018-12-20])) 23 | 24 | Process.exit(john, :kill) 25 | 26 | entries = 27 | "john" 28 | |> Todo.Cache.server_process() 29 | |> Todo.Server.entries(~D[2018-12-20]) 30 | 31 | assert [%{date: ~D[2018-12-20], title: "Shopping"}] = entries 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_env/wrk.lua: -------------------------------------------------------------------------------- 1 | -- Instructions: 2 | -- 3 | -- 1. Make sure to delete the `persist` folder before starting the test 4 | -- 2. Start the system in prod environment: MIX_ENV=prod iex -S mix 5 | -- 3. In a separate shell, invoke wrk 6 | -- The command I've used: wrk -t4 -c28 -d30s --latency -s wrk.lua http://localhost:5454 7 | 8 | counter = 0 9 | 10 | request = function() 11 | which = "list_"..math.random(1000) 12 | day = math.random(31) 13 | if day < 10 then day = "0" .. day end 14 | if math.random(5) < 5 then 15 | wrk.method = "GET" 16 | path = "/entries?list="..which.."&date=2018-12-"..day 17 | else 18 | wrk.method = "POST" 19 | path = "/add_entry?list="..which.."&date=2018-12-"..day.."&title=Dentist" 20 | end 21 | return wrk.format(nil, path) 22 | end 23 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_poolboy/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_poolboy/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | /persist/ 6 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_poolboy/README.md: -------------------------------------------------------------------------------- 1 | This is a demo project for Elixir in Action. 2 | 3 | To be able to compile and run it, you'll need Elixir 1.0.0 and Erlang 17.0. A `git` client should also be installed and available somewhere in the execution path. 4 | 5 | Prior to the first build, you need to call `mix deps.get`. After that, you can build and start the interactive shell by running `iex -S mix` from this project root. -------------------------------------------------------------------------------- /code_samples/ch11/todo_poolboy/lib/todo/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Application do 2 | use Application 3 | 4 | @impl Application 5 | def start(_, _) do 6 | Todo.System.start_link() 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_poolboy/lib/todo/cache.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Cache do 2 | def start_link() do 3 | IO.puts("Starting to-do cache.") 4 | DynamicSupervisor.start_link(name: __MODULE__, strategy: :one_for_one) 5 | end 6 | 7 | def child_spec(_arg) do 8 | %{ 9 | id: __MODULE__, 10 | start: {__MODULE__, :start_link, []}, 11 | type: :supervisor 12 | } 13 | end 14 | 15 | def server_process(todo_list_name) do 16 | case start_child(todo_list_name) do 17 | {:ok, pid} -> pid 18 | {:error, {:already_started, pid}} -> pid 19 | end 20 | end 21 | 22 | defp start_child(todo_list_name) do 23 | DynamicSupervisor.start_child(__MODULE__, {Todo.Server, todo_list_name}) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_poolboy/lib/todo/database.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Database do 2 | @db_folder "./persist" 3 | 4 | def child_spec(_) do 5 | File.mkdir_p!(@db_folder) 6 | 7 | :poolboy.child_spec( 8 | __MODULE__, 9 | [ 10 | name: {:local, __MODULE__}, 11 | worker_module: Todo.DatabaseWorker, 12 | size: 3 13 | ], 14 | [@db_folder] 15 | ) 16 | end 17 | 18 | def store(key, data) do 19 | :poolboy.transaction( 20 | __MODULE__, 21 | fn worker_pid -> Todo.DatabaseWorker.store(worker_pid, key, data) end 22 | ) 23 | end 24 | 25 | def get(key) do 26 | :poolboy.transaction( 27 | __MODULE__, 28 | fn worker_pid -> Todo.DatabaseWorker.get(worker_pid, key) end 29 | ) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_poolboy/lib/todo/database_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.DatabaseWorker do 2 | use GenServer 3 | 4 | def start_link(db_folder) do 5 | GenServer.start_link(__MODULE__, db_folder) 6 | end 7 | 8 | def store(pid, key, data) do 9 | GenServer.cast(pid, {:store, key, data}) 10 | end 11 | 12 | def get(pid, key) do 13 | GenServer.call(pid, {:get, key}) 14 | end 15 | 16 | @impl GenServer 17 | def init(db_folder) do 18 | IO.puts("Starting database worker.") 19 | {:ok, db_folder} 20 | end 21 | 22 | @impl GenServer 23 | def handle_cast({:store, key, data}, db_folder) do 24 | db_folder 25 | |> file_name(key) 26 | |> File.write!(:erlang.term_to_binary(data)) 27 | 28 | {:noreply, db_folder} 29 | end 30 | 31 | @impl GenServer 32 | def handle_call({:get, key}, _, db_folder) do 33 | data = 34 | case File.read(file_name(db_folder, key)) do 35 | {:ok, contents} -> :erlang.binary_to_term(contents) 36 | _ -> nil 37 | end 38 | 39 | {:reply, data, db_folder} 40 | end 41 | 42 | defp file_name(db_folder, key) do 43 | Path.join(db_folder, to_string(key)) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_poolboy/lib/todo/list.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.List do 2 | defstruct next_id: 1, entries: %{} 3 | 4 | def new(entries \\ []) do 5 | Enum.reduce( 6 | entries, 7 | %Todo.List{}, 8 | &add_entry(&2, &1) 9 | ) 10 | end 11 | 12 | def size(todo_list) do 13 | map_size(todo_list.entries) 14 | end 15 | 16 | def add_entry(todo_list, entry) do 17 | entry = Map.put(entry, :id, todo_list.next_id) 18 | new_entries = Map.put(todo_list.entries, todo_list.next_id, entry) 19 | 20 | %Todo.List{todo_list | entries: new_entries, next_id: todo_list.next_id + 1} 21 | end 22 | 23 | def entries(todo_list, date) do 24 | todo_list.entries 25 | |> Map.values() 26 | |> Enum.filter(fn entry -> entry.date == date end) 27 | end 28 | 29 | def update_entry(todo_list, entry_id, updater_fun) do 30 | case Map.fetch(todo_list.entries, entry_id) do 31 | :error -> 32 | todo_list 33 | 34 | {:ok, old_entry} -> 35 | new_entry = updater_fun.(old_entry) 36 | new_entries = Map.put(todo_list.entries, new_entry.id, new_entry) 37 | %Todo.List{todo_list | entries: new_entries} 38 | end 39 | end 40 | 41 | def delete_entry(todo_list, entry_id) do 42 | %Todo.List{todo_list | entries: Map.delete(todo_list.entries, entry_id)} 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_poolboy/lib/todo/metrics.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Metrics do 2 | use Task 3 | 4 | def start_link(_arg) do 5 | Task.start_link(&loop/0) 6 | end 7 | 8 | defp loop() do 9 | Process.sleep(:timer.seconds(10)) 10 | IO.inspect(collect_metrics()) 11 | loop() 12 | end 13 | 14 | defp collect_metrics() do 15 | [ 16 | memory_usage: :erlang.memory(:total), 17 | process_count: :erlang.system_info(:process_count) 18 | ] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_poolboy/lib/todo/process_registry.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.ProcessRegistry do 2 | def start_link do 3 | Registry.start_link(keys: :unique, name: __MODULE__) 4 | end 5 | 6 | def via_tuple(key) do 7 | {:via, Registry, {__MODULE__, key}} 8 | end 9 | 10 | def child_spec(_) do 11 | Supervisor.child_spec( 12 | Registry, 13 | id: __MODULE__, 14 | start: {__MODULE__, :start_link, []} 15 | ) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_poolboy/lib/todo/system.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.System do 2 | def start_link do 3 | Supervisor.start_link( 4 | [ 5 | Todo.Metrics, 6 | Todo.ProcessRegistry, 7 | Todo.Database, 8 | Todo.Cache 9 | ], 10 | strategy: :one_for_one 11 | ) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_poolboy/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :todo, 7 | version: "0.1.0", 8 | elixir: "~> 1.15", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | def application do 15 | [ 16 | extra_applications: [:logger], 17 | mod: {Todo.Application, []} 18 | ] 19 | end 20 | 21 | defp deps do 22 | [ 23 | {:poolboy, "~> 1.5"} 24 | ] 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_poolboy/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, 3 | } 4 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_poolboy/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | File.rm_rf!("./persist") 2 | File.mkdir_p!("./persist") 3 | ExUnit.start() 4 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_poolboy/test/todo/cache_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.CacheTest do 2 | use ExUnit.Case 3 | 4 | test "server_process" do 5 | bob_pid = Todo.Cache.server_process("bob") 6 | 7 | assert bob_pid != Todo.Cache.server_process("alice") 8 | assert bob_pid == Todo.Cache.server_process("bob") 9 | end 10 | 11 | test "to-do operations" do 12 | jane = Todo.Cache.server_process("jane") 13 | Todo.Server.add_entry(jane, %{date: ~D[2018-12-19], title: "Dentist"}) 14 | entries = Todo.Server.entries(jane, ~D[2018-12-19]) 15 | 16 | assert [%{date: ~D[2018-12-19], title: "Dentist"}] = entries 17 | end 18 | 19 | test "persistence" do 20 | john = Todo.Cache.server_process("john") 21 | Todo.Server.add_entry(john, %{date: ~D[2018-12-20], title: "Shopping"}) 22 | assert 1 == length(Todo.Server.entries(john, ~D[2018-12-20])) 23 | 24 | Process.exit(john, :kill) 25 | 26 | entries = 27 | "john" 28 | |> Todo.Cache.server_process() 29 | |> Todo.Server.entries(~D[2018-12-20]) 30 | 31 | assert [%{date: ~D[2018-12-20], title: "Shopping"}] = entries 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_web/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_web/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | /persist/ 6 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_web/README.md: -------------------------------------------------------------------------------- 1 | This is a demo project for Elixir in Action. 2 | 3 | To be able to compile and run it, you'll need Elixir 1.0.0 and Erlang 17.0. A `git` client should also be installed and available somewhere in the execution path. 4 | 5 | Prior to the first build, you need to call `mix deps.get`. After that, you can build and start the interactive shell by running `iex -S mix` from this project root. -------------------------------------------------------------------------------- /code_samples/ch11/todo_web/lib/todo/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Application do 2 | use Application 3 | 4 | @impl Application 5 | def start(_, _) do 6 | Todo.System.start_link() 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_web/lib/todo/cache.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Cache do 2 | def start_link() do 3 | IO.puts("Starting to-do cache.") 4 | DynamicSupervisor.start_link(name: __MODULE__, strategy: :one_for_one) 5 | end 6 | 7 | def child_spec(_arg) do 8 | %{ 9 | id: __MODULE__, 10 | start: {__MODULE__, :start_link, []}, 11 | type: :supervisor 12 | } 13 | end 14 | 15 | def server_process(todo_list_name) do 16 | case start_child(todo_list_name) do 17 | {:ok, pid} -> pid 18 | {:error, {:already_started, pid}} -> pid 19 | end 20 | end 21 | 22 | defp start_child(todo_list_name) do 23 | DynamicSupervisor.start_child(__MODULE__, {Todo.Server, todo_list_name}) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_web/lib/todo/database.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Database do 2 | @db_folder "./persist" 3 | 4 | def child_spec(_) do 5 | File.mkdir_p!(@db_folder) 6 | 7 | :poolboy.child_spec( 8 | __MODULE__, 9 | [ 10 | name: {:local, __MODULE__}, 11 | worker_module: Todo.DatabaseWorker, 12 | size: 3 13 | ], 14 | [@db_folder] 15 | ) 16 | end 17 | 18 | def store(key, data) do 19 | :poolboy.transaction(__MODULE__, fn worker_pid -> 20 | Todo.DatabaseWorker.store(worker_pid, key, data) 21 | end) 22 | end 23 | 24 | def get(key) do 25 | :poolboy.transaction(__MODULE__, fn worker_pid -> Todo.DatabaseWorker.get(worker_pid, key) end) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_web/lib/todo/database_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.DatabaseWorker do 2 | use GenServer 3 | 4 | def start_link(db_folder) do 5 | GenServer.start_link(__MODULE__, db_folder) 6 | end 7 | 8 | def store(pid, key, data) do 9 | GenServer.cast(pid, {:store, key, data}) 10 | end 11 | 12 | def get(pid, key) do 13 | GenServer.call(pid, {:get, key}) 14 | end 15 | 16 | @impl GenServer 17 | def init(db_folder) do 18 | IO.puts("Starting database worker.") 19 | {:ok, db_folder} 20 | end 21 | 22 | @impl GenServer 23 | def handle_cast({:store, key, data}, db_folder) do 24 | db_folder 25 | |> file_name(key) 26 | |> File.write!(:erlang.term_to_binary(data)) 27 | 28 | {:noreply, db_folder} 29 | end 30 | 31 | @impl GenServer 32 | def handle_call({:get, key}, _, db_folder) do 33 | data = 34 | case File.read(file_name(db_folder, key)) do 35 | {:ok, contents} -> :erlang.binary_to_term(contents) 36 | _ -> nil 37 | end 38 | 39 | {:reply, data, db_folder} 40 | end 41 | 42 | defp file_name(db_folder, key) do 43 | Path.join(db_folder, to_string(key)) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_web/lib/todo/list.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.List do 2 | defstruct next_id: 1, entries: %{} 3 | 4 | def new(entries \\ []) do 5 | Enum.reduce( 6 | entries, 7 | %Todo.List{}, 8 | &add_entry(&2, &1) 9 | ) 10 | end 11 | 12 | def size(todo_list) do 13 | map_size(todo_list.entries) 14 | end 15 | 16 | def add_entry(todo_list, entry) do 17 | entry = Map.put(entry, :id, todo_list.next_id) 18 | new_entries = Map.put(todo_list.entries, todo_list.next_id, entry) 19 | 20 | %Todo.List{todo_list | entries: new_entries, next_id: todo_list.next_id + 1} 21 | end 22 | 23 | def entries(todo_list, date) do 24 | todo_list.entries 25 | |> Map.values() 26 | |> Enum.filter(fn entry -> entry.date == date end) 27 | end 28 | 29 | def update_entry(todo_list, entry_id, updater_fun) do 30 | case Map.fetch(todo_list.entries, entry_id) do 31 | :error -> 32 | todo_list 33 | 34 | {:ok, old_entry} -> 35 | new_entry = updater_fun.(old_entry) 36 | new_entries = Map.put(todo_list.entries, new_entry.id, new_entry) 37 | %Todo.List{todo_list | entries: new_entries} 38 | end 39 | end 40 | 41 | def delete_entry(todo_list, entry_id) do 42 | %Todo.List{todo_list | entries: Map.delete(todo_list.entries, entry_id)} 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_web/lib/todo/metrics.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Metrics do 2 | use Task 3 | 4 | def start_link(_arg) do 5 | Task.start_link(&loop/0) 6 | end 7 | 8 | defp loop() do 9 | Process.sleep(:timer.seconds(10)) 10 | IO.inspect(collect_metrics()) 11 | loop() 12 | end 13 | 14 | defp collect_metrics() do 15 | [ 16 | memory_usage: :erlang.memory(:total), 17 | process_count: :erlang.system_info(:process_count) 18 | ] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_web/lib/todo/process_registry.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.ProcessRegistry do 2 | def start_link do 3 | Registry.start_link(keys: :unique, name: __MODULE__) 4 | end 5 | 6 | def via_tuple(key) do 7 | {:via, Registry, {__MODULE__, key}} 8 | end 9 | 10 | def child_spec(_) do 11 | Supervisor.child_spec( 12 | Registry, 13 | id: __MODULE__, 14 | start: {__MODULE__, :start_link, []} 15 | ) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_web/lib/todo/system.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.System do 2 | def start_link do 3 | Supervisor.start_link( 4 | [ 5 | Todo.Metrics, 6 | Todo.ProcessRegistry, 7 | Todo.Database, 8 | Todo.Cache, 9 | Todo.Web 10 | ], 11 | strategy: :one_for_one 12 | ) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_web/lib/todo/web.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Web do 2 | use Plug.Router 3 | 4 | plug(:match) 5 | plug(:dispatch) 6 | 7 | def child_spec(_arg) do 8 | Plug.Cowboy.child_spec( 9 | scheme: :http, 10 | options: [port: 5454], 11 | plug: __MODULE__ 12 | ) 13 | end 14 | 15 | # curl 'http://localhost:5454/entries?list=bob&date=2018-12-19' 16 | get "/entries" do 17 | conn = Plug.Conn.fetch_query_params(conn) 18 | list_name = Map.fetch!(conn.params, "list") 19 | date = Date.from_iso8601!(Map.fetch!(conn.params, "date")) 20 | 21 | entries = 22 | list_name 23 | |> Todo.Cache.server_process() 24 | |> Todo.Server.entries(date) 25 | 26 | formatted_entries = 27 | entries 28 | |> Enum.map(&"#{&1.date} #{&1.title}") 29 | |> Enum.join("\n") 30 | 31 | conn 32 | |> Plug.Conn.put_resp_content_type("text/plain") 33 | |> Plug.Conn.send_resp(200, formatted_entries) 34 | end 35 | 36 | # curl -d '' 'http://localhost:5454/add_entry?list=bob&date=2018-12-19&title=Dentist' 37 | post "/add_entry" do 38 | conn = Plug.Conn.fetch_query_params(conn) 39 | list_name = Map.fetch!(conn.params, "list") 40 | title = Map.fetch!(conn.params, "title") 41 | date = Date.from_iso8601!(Map.fetch!(conn.params, "date")) 42 | 43 | list_name 44 | |> Todo.Cache.server_process() 45 | |> Todo.Server.add_entry(%{title: title, date: date}) 46 | 47 | conn 48 | |> Plug.Conn.put_resp_content_type("text/plain") 49 | |> Plug.Conn.send_resp(200, "OK") 50 | end 51 | 52 | match _ do 53 | Plug.Conn.send_resp(conn, 404, "not found") 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_web/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :todo, 7 | version: "0.1.0", 8 | elixir: "~> 1.15", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | def application do 15 | [ 16 | extra_applications: [:logger], 17 | mod: {Todo.Application, []} 18 | ] 19 | end 20 | 21 | defp deps do 22 | [ 23 | {:poolboy, "~> 1.5"}, 24 | {:plug_cowboy, "~> 2.6"} 25 | ] 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_web/test/http_server_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HttpServerTest do 2 | use ExUnit.Case, async: false 3 | use Plug.Test 4 | 5 | test "no entries" do 6 | assert get("/entries?list=test_1&date=2018-12-19").status == 200 7 | assert get("/entries?list=test_1&date=2018-12-19").resp_body == "" 8 | end 9 | 10 | test "adding an entry" do 11 | resp = post("/add_entry?list=test_2&date=2018-12-19&title=Dentist") 12 | 13 | assert resp.status == 200 14 | assert resp.resp_body == "OK" 15 | assert get("/entries?list=test_2&date=2018-12-19").resp_body == "2018-12-19 Dentist" 16 | end 17 | 18 | defp get(path) do 19 | Todo.Web.call(conn(:get, path), Todo.Web.init([])) 20 | end 21 | 22 | defp post(path) do 23 | Todo.Web.call(conn(:post, path), Todo.Web.init([])) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_web/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | File.rm_rf!("./persist") 2 | File.mkdir_p!("./persist") 3 | ExUnit.start() 4 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_web/test/todo/cache_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.CacheTest do 2 | use ExUnit.Case 3 | 4 | test "server_process" do 5 | bob_pid = Todo.Cache.server_process("bob") 6 | 7 | assert bob_pid != Todo.Cache.server_process("alice") 8 | assert bob_pid == Todo.Cache.server_process("bob") 9 | end 10 | 11 | test "to-do operations" do 12 | jane = Todo.Cache.server_process("jane") 13 | Todo.Server.add_entry(jane, %{date: ~D[2018-12-19], title: "Dentist"}) 14 | entries = Todo.Server.entries(jane, ~D[2018-12-19]) 15 | 16 | assert [%{date: ~D[2018-12-19], title: "Dentist"}] = entries 17 | end 18 | 19 | test "persistence" do 20 | john = Todo.Cache.server_process("john") 21 | Todo.Server.add_entry(john, %{date: ~D[2018-12-20], title: "Shopping"}) 22 | assert 1 == length(Todo.Server.entries(john, ~D[2018-12-20])) 23 | 24 | Process.exit(john, :kill) 25 | 26 | entries = 27 | "john" 28 | |> Todo.Cache.server_process() 29 | |> Todo.Server.entries(~D[2018-12-20]) 30 | 31 | assert [%{date: ~D[2018-12-20], title: "Shopping"}] = entries 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /code_samples/ch11/todo_web/wrk.lua: -------------------------------------------------------------------------------- 1 | -- Instructions: 2 | -- 3 | -- 1. Make sure to delete the `persist` folder before starting the test 4 | -- 2. Start the system in prod environment: MIX_ENV=prod iex -S mix 5 | -- 3. In a separate shell, invoke wrk 6 | -- The command I've used: wrk -t4 -c28 -d30s --latency -s wrk.lua http://localhost:5454 7 | 8 | counter = 0 9 | 10 | request = function() 11 | which = "list_"..math.random(1000) 12 | day = math.random(31) 13 | if day < 10 then day = "0" .. day end 14 | if math.random(5) < 5 then 15 | wrk.method = "GET" 16 | path = "/entries?list="..which.."&date=2018-12-"..day 17 | else 18 | wrk.method = "POST" 19 | path = "/add_entry?list="..which.."&date=2018-12-"..day.."&title=Dentist" 20 | end 21 | return wrk.format(nil, path) 22 | end 23 | -------------------------------------------------------------------------------- /code_samples/ch12/todo_distributed/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /code_samples/ch12/todo_distributed/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | /persist/ 6 | /test_persist/ 7 | -------------------------------------------------------------------------------- /code_samples/ch12/todo_distributed/README.md: -------------------------------------------------------------------------------- 1 | This is a demo project for Elixir in Action. 2 | 3 | To be able to compile and run it, you'll need Elixir 1.0.0 and Erlang 17.0. A `git` client should also be installed and available somewhere in the execution path. 4 | 5 | Prior to the first build, you need to call `mix deps.get`. After that, you can build and start the interactive shell by running `iex -S mix` from this project root. -------------------------------------------------------------------------------- /code_samples/ch12/todo_distributed/config/runtime.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # Using a different http port in test to allow running tests while the iex shell session is running. 4 | http_port = 5 | if config_env() != :test, 6 | do: System.get_env("TODO_HTTP_PORT", "5454"), 7 | else: System.get_env("TODO_TEST_HTTP_PORT", "5455") 8 | 9 | config :todo, http_port: String.to_integer(http_port) 10 | 11 | # Using a different db_folder in test to avoid polluting the dev db. 12 | db_folder = 13 | if config_env() != :test, 14 | do: System.get_env("TODO_DB_FOLDER", "./persist"), 15 | else: System.get_env("TODO_TEST_DB_FOLDER", "./test_persist") 16 | 17 | config :todo, :database, db_folder: db_folder 18 | 19 | # Using a shorter to-do server expiry in local dev. 20 | todo_server_expiry = 21 | if config_env() != :dev, 22 | do: System.get_env("TODO_SERVER_EXPIRY", "60"), 23 | else: System.get_env("TODO_SERVER_EXPIRY", "10") 24 | 25 | config :todo, todo_server_expiry: :timer.seconds(String.to_integer(todo_server_expiry)) 26 | -------------------------------------------------------------------------------- /code_samples/ch12/todo_distributed/lib/todo/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Application do 2 | use Application 3 | 4 | @impl Application 5 | def start(_, _) do 6 | Todo.System.start_link() 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /code_samples/ch12/todo_distributed/lib/todo/cache.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Cache do 2 | def start_link() do 3 | IO.puts("Starting to-do cache.") 4 | DynamicSupervisor.start_link(name: __MODULE__, strategy: :one_for_one) 5 | end 6 | 7 | def child_spec(_arg) do 8 | %{ 9 | id: __MODULE__, 10 | start: {__MODULE__, :start_link, []}, 11 | type: :supervisor 12 | } 13 | end 14 | 15 | def server_process(todo_list_name) do 16 | existing_process(todo_list_name) || new_process(todo_list_name) 17 | end 18 | 19 | defp existing_process(todo_list_name) do 20 | Todo.Server.whereis(todo_list_name) 21 | end 22 | 23 | defp new_process(todo_list_name) do 24 | case DynamicSupervisor.start_child(__MODULE__, {Todo.Server, todo_list_name}) do 25 | {:ok, pid} -> pid 26 | {:error, {:already_started, pid}} -> pid 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /code_samples/ch12/todo_distributed/lib/todo/database.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Database do 2 | def child_spec(_) do 3 | db_settings = Application.fetch_env!(:todo, :database) 4 | 5 | # Node name is used to determine the database folder. This allows us to 6 | # start multiple nodes from the same folders, and data will not clash. 7 | [name_prefix, _] = "#{node()}" |> String.split("@") 8 | db_folder = "#{Keyword.fetch!(db_settings, :db_folder)}/#{name_prefix}/" 9 | 10 | File.mkdir_p!(db_folder) 11 | 12 | :poolboy.child_spec( 13 | __MODULE__, 14 | [ 15 | name: {:local, __MODULE__}, 16 | worker_module: Todo.DatabaseWorker, 17 | size: 3 18 | ], 19 | [db_folder] 20 | ) 21 | end 22 | 23 | def store(key, data) do 24 | {_results, bad_nodes} = 25 | :rpc.multicall( 26 | __MODULE__, 27 | :store_local, 28 | [key, data], 29 | :timer.seconds(5) 30 | ) 31 | 32 | Enum.each(bad_nodes, &IO.puts("Store failed on node #{&1}")) 33 | :ok 34 | end 35 | 36 | def store_local(key, data) do 37 | :poolboy.transaction(__MODULE__, fn worker_pid -> 38 | Todo.DatabaseWorker.store(worker_pid, key, data) 39 | end) 40 | end 41 | 42 | def get(key) do 43 | :poolboy.transaction(__MODULE__, fn worker_pid -> Todo.DatabaseWorker.get(worker_pid, key) end) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /code_samples/ch12/todo_distributed/lib/todo/database_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.DatabaseWorker do 2 | use GenServer 3 | 4 | def start_link(db_folder) do 5 | GenServer.start_link(__MODULE__, db_folder) 6 | end 7 | 8 | def store(pid, key, data) do 9 | GenServer.call(pid, {:store, key, data}) 10 | end 11 | 12 | def get(pid, key) do 13 | GenServer.call(pid, {:get, key}) 14 | end 15 | 16 | @impl GenServer 17 | def init(db_folder) do 18 | IO.puts("Starting database worker.") 19 | {:ok, db_folder} 20 | end 21 | 22 | @impl GenServer 23 | def handle_call({:store, key, data}, _, db_folder) do 24 | db_folder 25 | |> file_name(key) 26 | |> File.write!(:erlang.term_to_binary(data)) 27 | 28 | {:reply, :ok, db_folder} 29 | end 30 | 31 | def handle_call({:get, key}, _, db_folder) do 32 | data = 33 | case File.read(file_name(db_folder, key)) do 34 | {:ok, contents} -> :erlang.binary_to_term(contents) 35 | _ -> nil 36 | end 37 | 38 | {:reply, data, db_folder} 39 | end 40 | 41 | defp file_name(db_folder, key) do 42 | Path.join(db_folder, to_string(key)) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /code_samples/ch12/todo_distributed/lib/todo/list.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.List do 2 | defstruct next_id: 1, entries: %{} 3 | 4 | def new(entries \\ []) do 5 | Enum.reduce( 6 | entries, 7 | %Todo.List{}, 8 | &add_entry(&2, &1) 9 | ) 10 | end 11 | 12 | def size(todo_list) do 13 | map_size(todo_list.entries) 14 | end 15 | 16 | def add_entry(todo_list, entry) do 17 | entry = Map.put(entry, :id, todo_list.next_id) 18 | new_entries = Map.put(todo_list.entries, todo_list.next_id, entry) 19 | 20 | %Todo.List{todo_list | entries: new_entries, next_id: todo_list.next_id + 1} 21 | end 22 | 23 | def entries(todo_list, date) do 24 | todo_list.entries 25 | |> Map.values() 26 | |> Enum.filter(fn entry -> entry.date == date end) 27 | end 28 | 29 | def update_entry(todo_list, entry_id, updater_fun) do 30 | case Map.fetch(todo_list.entries, entry_id) do 31 | :error -> 32 | todo_list 33 | 34 | {:ok, old_entry} -> 35 | new_entry = updater_fun.(old_entry) 36 | new_entries = Map.put(todo_list.entries, new_entry.id, new_entry) 37 | %Todo.List{todo_list | entries: new_entries} 38 | end 39 | end 40 | 41 | def delete_entry(todo_list, entry_id) do 42 | %Todo.List{todo_list | entries: Map.delete(todo_list.entries, entry_id)} 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /code_samples/ch12/todo_distributed/lib/todo/metrics.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Metrics do 2 | use Task 3 | 4 | def start_link(_arg) do 5 | Task.start_link(&loop/0) 6 | end 7 | 8 | defp loop() do 9 | Process.sleep(:timer.seconds(10)) 10 | IO.inspect(collect_metrics()) 11 | loop() 12 | end 13 | 14 | defp collect_metrics() do 15 | [ 16 | memory_usage: :erlang.memory(:total), 17 | process_count: :erlang.system_info(:process_count) 18 | ] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /code_samples/ch12/todo_distributed/lib/todo/system.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.System do 2 | def start_link do 3 | Supervisor.start_link( 4 | [ 5 | Todo.Metrics, 6 | Todo.Database, 7 | Todo.Cache, 8 | Todo.Web 9 | ], 10 | strategy: :one_for_one 11 | ) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /code_samples/ch12/todo_distributed/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :todo, 7 | version: "0.1.0", 8 | elixir: "~> 1.15", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | def application do 15 | [ 16 | extra_applications: [:logger], 17 | mod: {Todo.Application, []} 18 | ] 19 | end 20 | 21 | defp deps do 22 | [ 23 | {:poolboy, "~> 1.5"}, 24 | {:plug_cowboy, "~> 2.6"} 25 | ] 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /code_samples/ch12/todo_distributed/test/http_server_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HttpServerTest do 2 | use ExUnit.Case, async: false 3 | use Plug.Test 4 | 5 | test "no entries" do 6 | assert get("/entries?list=test_1&date=2018-12-19").status == 200 7 | assert get("/entries?list=test_1&date=2018-12-19").resp_body == "" 8 | end 9 | 10 | test "adding an entry" do 11 | resp = post("/add_entry?list=test_2&date=2018-12-19&title=Dentist") 12 | 13 | assert resp.status == 200 14 | assert resp.resp_body == "OK" 15 | assert get("/entries?list=test_2&date=2018-12-19").resp_body == "2018-12-19 Dentist" 16 | end 17 | 18 | defp get(path) do 19 | Todo.Web.call(conn(:get, path), Todo.Web.init([])) 20 | end 21 | 22 | defp post(path) do 23 | Todo.Web.call(conn(:post, path), Todo.Web.init([])) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /code_samples/ch12/todo_distributed/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | db_settings = Application.fetch_env!(:todo, :database) 2 | db_folder = Path.join(Keyword.fetch!(db_settings, :db_folder), "nonode") 3 | File.rm_rf!(db_folder) 4 | File.mkdir_p!(db_folder) 5 | ExUnit.start() 6 | -------------------------------------------------------------------------------- /code_samples/ch12/todo_distributed/test/todo/cache_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.CacheTest do 2 | use ExUnit.Case 3 | 4 | test "server_process" do 5 | bob_pid = Todo.Cache.server_process("bob") 6 | 7 | assert bob_pid != Todo.Cache.server_process("alice") 8 | assert bob_pid == Todo.Cache.server_process("bob") 9 | end 10 | 11 | test "to-do operations" do 12 | jane = Todo.Cache.server_process("jane") 13 | Todo.Server.add_entry(jane, %{date: ~D[2018-12-19], title: "Dentist"}) 14 | entries = Todo.Server.entries(jane, ~D[2018-12-19]) 15 | 16 | assert [%{date: ~D[2018-12-19], title: "Dentist"}] = entries 17 | end 18 | 19 | test "persistence" do 20 | john = Todo.Cache.server_process("john") 21 | Todo.Server.add_entry(john, %{date: ~D[2018-12-20], title: "Shopping"}) 22 | assert 1 == length(Todo.Server.entries(john, ~D[2018-12-20])) 23 | 24 | Process.exit(john, :kill) 25 | 26 | entries = 27 | "john" 28 | |> Todo.Cache.server_process() 29 | |> Todo.Server.entries(~D[2018-12-20]) 30 | 31 | assert [%{date: ~D[2018-12-20], title: "Shopping"}] = entries 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /code_samples/ch12/todo_distributed/wrk.lua: -------------------------------------------------------------------------------- 1 | -- Instructions: 2 | -- 3 | -- 1. Make sure to delete the `persist` folder before starting the test 4 | -- 2. Start the system in prod environment: MIX_ENV=prod iex -S mix 5 | -- 3. In a separate shell, invoke wrk 6 | -- The command I've used: wrk -t4 -c28 -d30s --latency -s wrk.lua http://localhost:5454 7 | 8 | counter = 0 9 | 10 | request = function() 11 | which = "list_"..math.random(1000) 12 | day = math.random(31) 13 | if day < 10 then day = "0" .. day end 14 | if math.random(5) < 5 then 15 | wrk.method = "GET" 16 | path = "/entries?list="..which.."&date=2018-12-"..day 17 | else 18 | wrk.method = "POST" 19 | path = "/add_entry?list="..which.."&date=2018-12-"..day.."&title=Dentist" 20 | end 21 | return wrk.format(nil, path) 22 | end 23 | -------------------------------------------------------------------------------- /code_samples/ch13/todo_release/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /code_samples/ch13/todo_release/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | /persist/ 6 | /test_persist/ 7 | -------------------------------------------------------------------------------- /code_samples/ch13/todo_release/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ELIXIR="1.15.4" 2 | ARG ERLANG="26.0.2" 3 | ARG DEBIAN="bookworm-20230612-slim" 4 | ARG OS="debian-${DEBIAN}" 5 | FROM "hexpm/elixir:${ELIXIR}-erlang-${ERLANG}-${OS}" as builder 6 | 7 | WORKDIR /todo 8 | ENV MIX_ENV="prod" 9 | 10 | RUN mix local.hex --force && mix local.rebar --force 11 | 12 | COPY mix.exs mix.lock ./ 13 | COPY config config 14 | COPY lib lib 15 | 16 | RUN mix deps.get --only prod 17 | RUN mix release 18 | 19 | 20 | FROM debian:${DEBIAN} 21 | 22 | WORKDIR "/todo" 23 | 24 | RUN apt-get update -y && apt-get install -y openssl locales 25 | 26 | COPY \ 27 | --from=builder \ 28 | --chown=nobody:root \ 29 | /todo/_build/prod/rel/todo ./ 30 | 31 | RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen 32 | ENV LANG en_US.UTF-8 33 | ENV LANGUAGE en_US:en 34 | ENV LC_ALL en_US.UTF-8 35 | 36 | CMD ["/todo/bin/todo", "start_iex"] 37 | -------------------------------------------------------------------------------- /code_samples/ch13/todo_release/README.md: -------------------------------------------------------------------------------- 1 | This is a demo project for Elixir in Action. 2 | 3 | To be able to compile and run it, you'll need Elixir 1.0.0 and Erlang 17.0. A `git` client should also be installed and available somewhere in the execution path. 4 | 5 | Prior to the first build, you need to call `mix deps.get`. After that, you can build and start the interactive shell by running `iex -S mix` from this project root. -------------------------------------------------------------------------------- /code_samples/ch13/todo_release/config/runtime.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # Using a different http port in test to allow running tests while the iex shell session is running. 4 | http_port = 5 | if config_env() != :test, 6 | do: System.get_env("TODO_HTTP_PORT", "5454"), 7 | else: System.get_env("TODO_TEST_HTTP_PORT", "5455") 8 | 9 | config :todo, http_port: String.to_integer(http_port) 10 | 11 | # Using a different db_folder in test to avoid polluting the dev db. 12 | db_folder = 13 | if config_env() != :test, 14 | do: System.get_env("TODO_DB_FOLDER", "./persist"), 15 | else: System.get_env("TODO_TEST_DB_FOLDER", "./test_persist") 16 | 17 | config :todo, :database, db_folder: db_folder 18 | 19 | # Using a shorter to-do server expiry in local dev. 20 | todo_server_expiry = 21 | if config_env() != :dev, 22 | do: System.get_env("TODO_SERVER_EXPIRY", "60"), 23 | else: System.get_env("TODO_SERVER_EXPIRY", "10") 24 | 25 | config :todo, todo_server_expiry: :timer.seconds(String.to_integer(todo_server_expiry)) 26 | -------------------------------------------------------------------------------- /code_samples/ch13/todo_release/lib/todo/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Application do 2 | use Application 3 | 4 | @impl Application 5 | def start(_, _) do 6 | Todo.System.start_link() 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /code_samples/ch13/todo_release/lib/todo/cache.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Cache do 2 | def start_link() do 3 | IO.puts("Starting to-do cache.") 4 | DynamicSupervisor.start_link(name: __MODULE__, strategy: :one_for_one) 5 | end 6 | 7 | def child_spec(_arg) do 8 | %{ 9 | id: __MODULE__, 10 | start: {__MODULE__, :start_link, []}, 11 | type: :supervisor 12 | } 13 | end 14 | 15 | def server_process(todo_list_name) do 16 | existing_process(todo_list_name) || new_process(todo_list_name) 17 | end 18 | 19 | defp existing_process(todo_list_name) do 20 | Todo.Server.whereis(todo_list_name) 21 | end 22 | 23 | defp new_process(todo_list_name) do 24 | case DynamicSupervisor.start_child(__MODULE__, {Todo.Server, todo_list_name}) do 25 | {:ok, pid} -> pid 26 | {:error, {:already_started, pid}} -> pid 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /code_samples/ch13/todo_release/lib/todo/database.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Database do 2 | def child_spec(_) do 3 | db_settings = Application.fetch_env!(:todo, :database) 4 | 5 | # Node name is used to determine the database folder. This allows us to 6 | # start multiple nodes from the same folders, and data will not clash. 7 | [name_prefix, _] = "#{node()}" |> String.split("@") 8 | db_folder = "#{Keyword.fetch!(db_settings, :db_folder)}/#{name_prefix}/" 9 | 10 | File.mkdir_p!(db_folder) 11 | 12 | :poolboy.child_spec( 13 | __MODULE__, 14 | [ 15 | name: {:local, __MODULE__}, 16 | worker_module: Todo.DatabaseWorker, 17 | size: 3 18 | ], 19 | [db_folder] 20 | ) 21 | end 22 | 23 | def store(key, data) do 24 | {_results, bad_nodes} = 25 | :rpc.multicall( 26 | __MODULE__, 27 | :store_local, 28 | [key, data], 29 | :timer.seconds(5) 30 | ) 31 | 32 | Enum.each(bad_nodes, &IO.puts("Store failed on node #{&1}")) 33 | :ok 34 | end 35 | 36 | def store_local(key, data) do 37 | :poolboy.transaction(__MODULE__, fn worker_pid -> 38 | Todo.DatabaseWorker.store(worker_pid, key, data) 39 | end) 40 | end 41 | 42 | def get(key) do 43 | :poolboy.transaction(__MODULE__, fn worker_pid -> Todo.DatabaseWorker.get(worker_pid, key) end) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /code_samples/ch13/todo_release/lib/todo/database_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.DatabaseWorker do 2 | use GenServer 3 | 4 | def start_link(db_folder) do 5 | GenServer.start_link(__MODULE__, db_folder) 6 | end 7 | 8 | def store(pid, key, data) do 9 | GenServer.call(pid, {:store, key, data}) 10 | end 11 | 12 | def get(pid, key) do 13 | GenServer.call(pid, {:get, key}) 14 | end 15 | 16 | @impl GenServer 17 | def init(db_folder) do 18 | IO.puts("Starting database worker.") 19 | {:ok, db_folder} 20 | end 21 | 22 | @impl GenServer 23 | def handle_call({:store, key, data}, _, db_folder) do 24 | db_folder 25 | |> file_name(key) 26 | |> File.write!(:erlang.term_to_binary(data)) 27 | 28 | {:reply, :ok, db_folder} 29 | end 30 | 31 | def handle_call({:get, key}, _, db_folder) do 32 | data = 33 | case File.read(file_name(db_folder, key)) do 34 | {:ok, contents} -> :erlang.binary_to_term(contents) 35 | _ -> nil 36 | end 37 | 38 | {:reply, data, db_folder} 39 | end 40 | 41 | defp file_name(db_folder, key) do 42 | Path.join(db_folder, to_string(key)) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /code_samples/ch13/todo_release/lib/todo/list.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.List do 2 | defstruct next_id: 1, entries: %{} 3 | 4 | def new(entries \\ []) do 5 | Enum.reduce( 6 | entries, 7 | %Todo.List{}, 8 | &add_entry(&2, &1) 9 | ) 10 | end 11 | 12 | def size(todo_list) do 13 | map_size(todo_list.entries) 14 | end 15 | 16 | def add_entry(todo_list, entry) do 17 | entry = Map.put(entry, :id, todo_list.next_id) 18 | new_entries = Map.put(todo_list.entries, todo_list.next_id, entry) 19 | 20 | %Todo.List{todo_list | entries: new_entries, next_id: todo_list.next_id + 1} 21 | end 22 | 23 | def entries(todo_list, date) do 24 | todo_list.entries 25 | |> Map.values() 26 | |> Enum.filter(fn entry -> entry.date == date end) 27 | end 28 | 29 | def update_entry(todo_list, entry_id, updater_fun) do 30 | case Map.fetch(todo_list.entries, entry_id) do 31 | :error -> 32 | todo_list 33 | 34 | {:ok, old_entry} -> 35 | new_entry = updater_fun.(old_entry) 36 | new_entries = Map.put(todo_list.entries, new_entry.id, new_entry) 37 | %Todo.List{todo_list | entries: new_entries} 38 | end 39 | end 40 | 41 | def delete_entry(todo_list, entry_id) do 42 | %Todo.List{todo_list | entries: Map.delete(todo_list.entries, entry_id)} 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /code_samples/ch13/todo_release/lib/todo/metrics.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Metrics do 2 | use Task 3 | 4 | def start_link(_arg) do 5 | Task.start_link(&loop/0) 6 | end 7 | 8 | defp loop() do 9 | Process.sleep(:timer.seconds(10)) 10 | IO.inspect(collect_metrics()) 11 | loop() 12 | end 13 | 14 | defp collect_metrics() do 15 | [ 16 | memory_usage: :erlang.memory(:total), 17 | process_count: :erlang.system_info(:process_count) 18 | ] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /code_samples/ch13/todo_release/lib/todo/system.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.System do 2 | def start_link do 3 | Supervisor.start_link( 4 | [ 5 | Todo.Metrics, 6 | Todo.Database, 7 | Todo.Cache, 8 | Todo.Web 9 | ], 10 | strategy: :one_for_one 11 | ) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /code_samples/ch13/todo_release/lib/todo/web.ex: -------------------------------------------------------------------------------- 1 | defmodule Todo.Web do 2 | use Plug.Router 3 | 4 | plug(:match) 5 | plug(:dispatch) 6 | 7 | def child_spec(_arg) do 8 | Plug.Cowboy.child_spec( 9 | scheme: :http, 10 | options: [port: Application.fetch_env!(:todo, :http_port)], 11 | plug: __MODULE__ 12 | ) 13 | end 14 | 15 | # curl 'http://localhost:5454/entries?list=bob&date=2018-12-19' 16 | get "/entries" do 17 | conn = Plug.Conn.fetch_query_params(conn) 18 | list_name = Map.fetch!(conn.params, "list") 19 | date = Date.from_iso8601!(Map.fetch!(conn.params, "date")) 20 | 21 | entries = 22 | list_name 23 | |> Todo.Cache.server_process() 24 | |> Todo.Server.entries(date) 25 | 26 | formatted_entries = 27 | entries 28 | |> Enum.map(&"#{&1.date} #{&1.title}") 29 | |> Enum.join("\n") 30 | 31 | conn 32 | |> Plug.Conn.put_resp_content_type("text/plain") 33 | |> Plug.Conn.send_resp(200, formatted_entries) 34 | end 35 | 36 | # curl -d '' 'http://localhost:5454/add_entry?list=bob&date=2018-12-19&title=Dentist' 37 | post "/add_entry" do 38 | conn = Plug.Conn.fetch_query_params(conn) 39 | list_name = Map.fetch!(conn.params, "list") 40 | title = Map.fetch!(conn.params, "title") 41 | date = Date.from_iso8601!(Map.fetch!(conn.params, "date")) 42 | 43 | list_name 44 | |> Todo.Cache.server_process() 45 | |> Todo.Server.add_entry(%{title: title, date: date}) 46 | 47 | conn 48 | |> Plug.Conn.put_resp_content_type("text/plain") 49 | |> Plug.Conn.send_resp(200, "OK") 50 | end 51 | 52 | match _ do 53 | Plug.Conn.send_resp(conn, 404, "not found") 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /code_samples/ch13/todo_release/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :todo, 7 | version: "0.1.0", 8 | elixir: "~> 1.15", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | def application do 15 | [ 16 | extra_applications: [:logger, :runtime_tools], 17 | mod: {Todo.Application, []} 18 | ] 19 | end 20 | 21 | defp deps do 22 | [ 23 | {:poolboy, "~> 1.5"}, 24 | {:plug_cowboy, "~> 2.6"} 25 | ] 26 | end 27 | 28 | def cli do 29 | [ 30 | preferred_envs: [release: :prod] 31 | ] 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /code_samples/ch13/todo_release/test/http_server_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HttpServerTest do 2 | use ExUnit.Case, async: false 3 | use Plug.Test 4 | 5 | test "no entries" do 6 | assert get("/entries?list=test_1&date=2018-12-19").status == 200 7 | assert get("/entries?list=test_1&date=2018-12-19").resp_body == "" 8 | end 9 | 10 | test "adding an entry" do 11 | resp = post("/add_entry?list=test_2&date=2018-12-19&title=Dentist") 12 | 13 | assert resp.status == 200 14 | assert resp.resp_body == "OK" 15 | assert get("/entries?list=test_2&date=2018-12-19").resp_body == "2018-12-19 Dentist" 16 | end 17 | 18 | defp get(path) do 19 | Todo.Web.call(conn(:get, path), Todo.Web.init([])) 20 | end 21 | 22 | defp post(path) do 23 | Todo.Web.call(conn(:post, path), Todo.Web.init([])) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /code_samples/ch13/todo_release/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | db_settings = Application.fetch_env!(:todo, :database) 2 | db_folder = Path.join(Keyword.fetch!(db_settings, :db_folder), "nonode") 3 | File.rm_rf!(db_folder) 4 | File.mkdir_p!(db_folder) 5 | ExUnit.start() 6 | -------------------------------------------------------------------------------- /code_samples/ch13/todo_release/test/todo/cache_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.CacheTest do 2 | use ExUnit.Case 3 | 4 | test "server_process" do 5 | bob_pid = Todo.Cache.server_process("bob") 6 | 7 | assert bob_pid != Todo.Cache.server_process("alice") 8 | assert bob_pid == Todo.Cache.server_process("bob") 9 | end 10 | 11 | test "to-do operations" do 12 | jane = Todo.Cache.server_process("jane") 13 | Todo.Server.add_entry(jane, %{date: ~D[2018-12-19], title: "Dentist"}) 14 | entries = Todo.Server.entries(jane, ~D[2018-12-19]) 15 | 16 | assert [%{date: ~D[2018-12-19], title: "Dentist"}] = entries 17 | end 18 | 19 | test "persistence" do 20 | john = Todo.Cache.server_process("john") 21 | Todo.Server.add_entry(john, %{date: ~D[2018-12-20], title: "Shopping"}) 22 | assert 1 == length(Todo.Server.entries(john, ~D[2018-12-20])) 23 | 24 | Process.exit(john, :kill) 25 | 26 | entries = 27 | "john" 28 | |> Todo.Cache.server_process() 29 | |> Todo.Server.entries(~D[2018-12-20]) 30 | 31 | assert [%{date: ~D[2018-12-20], title: "Shopping"}] = entries 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /code_samples/ch13/todo_release/wrk.lua: -------------------------------------------------------------------------------- 1 | -- Instructions: 2 | -- 3 | -- 1. Make sure to delete the `persist` folder before starting the test 4 | -- 2. Start the system in prod environment: MIX_ENV=prod iex -S mix 5 | -- 3. In a separate shell, invoke wrk 6 | -- The command I've used: wrk -t4 -c28 -d30s --latency -s wrk.lua http://localhost:5454 7 | 8 | counter = 0 9 | 10 | request = function() 11 | which = "list_"..math.random(1000) 12 | day = math.random(31) 13 | if day < 10 then day = "0" .. day end 14 | if math.random(5) < 5 then 15 | wrk.method = "GET" 16 | path = "/entries?list="..which.."&date=2018-12-"..day 17 | else 18 | wrk.method = "POST" 19 | path = "/add_entry?list="..which.."&date=2018-12-"..day.."&title=Dentist" 20 | end 21 | return wrk.format(nil, path) 22 | end 23 | -------------------------------------------------------------------------------- /code_samples/test_helper.exs: -------------------------------------------------------------------------------- 1 | defmodule TestHelper do 2 | # Hacky helper that loads the script from the test, then runs the test 3 | # and finally unloads all modules defined by the script. 4 | defmacro test_script(script_name, opts) do 5 | quote do 6 | test unquote(script_name) do 7 | modules = Code.require_file("#{__DIR__}/../" <> unquote(script_name) <> ".ex") 8 | 9 | try do 10 | unquote(opts[:do]) 11 | after 12 | Enum.each(modules, &:code.purge(elem(&1, 0))) 13 | Enum.each(modules, &:code.delete(elem(&1, 0))) 14 | end 15 | end 16 | end 17 | end 18 | end 19 | 20 | ExUnit.start() 21 | --------------------------------------------------------------------------------