├── .gitignore ├── README.md ├── chapter_1 ├── recursive.erl └── recursive.ex ├── chapter_11 ├── concuerror_playground │ ├── .gitignore │ ├── .vimrc │ ├── README.md │ ├── config │ │ └── config.exs │ ├── lib │ │ ├── counter_server.ex │ │ ├── ping_pong.ex │ │ ├── ping_pong_reg.ex │ │ ├── reg_server.ex │ │ ├── spawn_reg.ex │ │ └── stacky.ex │ ├── mix.exs │ ├── ping_pong.dot │ ├── ping_pong.png │ ├── ping_pong_reg.dot │ ├── ping_pong_reg.png │ ├── spawn_link.dot │ ├── spawn_link.png │ ├── spawn_reg.dot │ ├── spawn_reg.png │ ├── stacky.dot │ ├── stacky.png │ └── test │ │ ├── concurrency │ │ ├── counter_server_test.ex │ │ ├── ping_pong_reg_test.ex │ │ ├── ping_pong_test.ex │ │ ├── reg_server_test.ex │ │ ├── spawn_link_test.ex │ │ ├── spawn_reg.ex │ │ ├── spawn_reg_test.ex │ │ └── stacky_test.ex │ │ └── test_helper.exs └── quickcheck_playground │ ├── .gitignore │ ├── .vimrc │ ├── README.md │ ├── config │ └── config.exs │ ├── lib │ └── quickcheck_playground.ex │ ├── mix.exs │ ├── mix.lock │ └── test │ ├── list_eqc.exs │ ├── map_eqc.exs │ ├── numbers_eqc.exs │ ├── others_eqc.exs │ ├── string_eqc.exs │ └── test_helper.exs ├── chapter_2 ├── 2_1 │ └── length_converter.ex ├── 2_2 │ ├── length_converter_2_4.ex │ └── length_converter_2_6.ex ├── 2_3 │ └── length_converter.ex └── 2_4 │ ├── id3.ex │ └── my_list.ex ├── chapter_3 └── metex │ ├── .gitignore │ ├── README.md │ ├── config │ └── config.exs │ ├── lib │ ├── coordinator.ex │ ├── metex.ex │ └── worker.ex │ ├── mix.exs │ ├── mix.lock │ └── test │ ├── metex_test.exs │ └── test_helper.exs ├── chapter_4 └── metex │ ├── .gitignore │ ├── README.md │ ├── config │ └── config.exs │ ├── lib │ ├── metex.ex │ └── worker.ex │ ├── mix.exs │ ├── mix.lock │ └── test │ ├── metex_test.exs │ └── test_helper.exs ├── chapter_5 └── thy_supervisor │ ├── .gitignore │ ├── README.md │ ├── config │ └── config.exs │ ├── lib │ ├── thy_supervisor.ex │ └── thy_worker.ex │ ├── mix.exs │ └── test │ ├── test_helper.exs │ └── thy_supervisor_test.exs ├── chapter_7 └── pooly │ ├── version-1 │ ├── README.md │ ├── config │ │ └── config.exs │ ├── lib │ │ ├── pooly.ex │ │ └── pooly │ │ │ ├── sample_worker.ex │ │ │ ├── server.ex │ │ │ ├── supervisor.ex │ │ │ └── worker_supervisor.ex │ ├── mix.exs │ └── test │ │ ├── pooly_test.exs │ │ └── test_helper.exs │ ├── version-2 │ ├── README.md │ ├── config │ │ └── config.exs │ ├── lib │ │ ├── pooly.ex │ │ └── pooly │ │ │ ├── sample_worker.ex │ │ │ ├── server.ex │ │ │ ├── supervisor.ex │ │ │ └── worker_supervisor.ex │ ├── mix.exs │ └── test │ │ ├── pooly_test.exs │ │ └── test_helper.exs │ ├── version-3 │ ├── README.md │ ├── config │ │ └── config.exs │ ├── lib │ │ ├── pooly.ex │ │ └── pooly │ │ │ ├── pool_server.ex │ │ │ ├── pool_supervisor.ex │ │ │ ├── pools_supervisor.ex │ │ │ ├── sample_worker.ex │ │ │ ├── server.ex │ │ │ ├── supervisor.ex │ │ │ └── worker_supervisor.ex │ ├── mix.exs │ └── test │ │ ├── pooly_test.exs │ │ └── test_helper.exs │ └── version-4 │ ├── README.md │ ├── config │ └── config.exs │ ├── lib │ ├── pooly.ex │ └── pooly │ │ ├── pool_server.ex │ │ ├── pool_supervisor.ex │ │ ├── pools_supervisor.ex │ │ ├── sample_worker.ex │ │ ├── server.ex │ │ ├── supervisor.ex │ │ └── worker_supervisor.ex │ ├── mix.exs │ └── test │ ├── pooly_test.exs │ └── test_helper.exs ├── chapter_8 └── blitzy │ ├── README.md │ ├── blitzy │ ├── config │ └── config.exs │ ├── lib │ ├── blitzy.ex │ ├── cli.ex │ ├── supervisor.ex │ └── worker.ex │ ├── mix.exs │ ├── mix.lock │ └── test │ ├── blitzy_test.exs │ └── test_helper.exs └── chapter_9 └── chucky ├── README.md ├── config ├── a.config ├── b.config ├── c.config └── config.exs ├── facts.txt ├── lib ├── chucky.ex └── server.ex ├── mix.exs └── test ├── chucky_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | *.beam 3 | _build 4 | deps 5 | erl_crash.dump 6 | *.ez 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Code for The Little Elixir & OTP Guidebook 2 | ======================================== 3 | 4 | Welcome! This is the source code for "The Little Elixir & OTP Guidebook". 5 | 6 | ![](http://i.imgur.com/bSJwGCJ.jpg) 7 | 8 | ## Running the code 9 | 10 | For programs that are not created by `mix`, can be run as such: 11 | 12 | ``` 13 | % iex length_converter.ex 14 | ``` 15 | 16 | Larger programs such as `metex` in Chapter 3, can be run with the following command: 17 | 18 | ``` 19 | % iex -S mix 20 | ``` 21 | 22 | These larger programs will contain their own READMEs and more detailed instructions on running the program. 23 | 24 | Enjoy! 25 | -------------------------------------------------------------------------------- /chapter_1/recursive.erl: -------------------------------------------------------------------------------- 1 | -module(recursive). 2 | -export([fac/1]). 3 | 4 | fac(N) when N == 0 -> 1; 5 | fac(N) when N > 0 -> N*fac(N-1). 6 | 7 | -------------------------------------------------------------------------------- /chapter_1/recursive.ex: -------------------------------------------------------------------------------- 1 | defmodule Recursive do 2 | def fac(n) when n == 0, do: 1 3 | def fac(n) when n > 0 do 4 | n*fac(n-1) 5 | end 6 | end 7 | 8 | -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | concuerror.* 7 | concuerror_report.txt 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/.vimrc: -------------------------------------------------------------------------------- 1 | " make sure the following 2 lines are included in ~/.vimrc 2 | " set exrc 3 | " set secure 4 | 5 | map t :!mix test 6 | map c :!mix compile && mix dialyzer 7 | -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/README.md: -------------------------------------------------------------------------------- 1 | # Concuerror Playground 2 | 3 | 1. Install [Concuerror](https://github.com/parapluu/Concuerror). 4 | 1. Make sure the tests all pass: `mix test`. 5 | 1. Here's an example command to run Concuerror on the `RegServer` module: 6 | 7 | ``` 8 | concuerror --pa /usr/local/Cellar/elixir/HEAD/lib/elixir/ebin/ \ 9 | --pa /usr/local/Cellar/elixir/HEAD/lib/ex_unit/ebin \ 10 | --pa _build/test/lib/concuerror_playground/ebin \ 11 | -m Elixir.RegServer.ConcurrencyTest \ 12 | --graph concuerror.dot \ 13 | --show_races true 14 | ``` 15 | -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :concuerror_playground, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:concuerror_playground, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/lib/counter_server.ex: -------------------------------------------------------------------------------- 1 | defmodule CounterServer do 2 | use GenServer 3 | 4 | @server __MODULE__ 5 | @table :table 6 | 7 | def start_link do 8 | GenServer.start_link(__MODULE__, [], name: @server) 9 | end 10 | 11 | def count_down do 12 | GenServer.call(@server, :count_down) 13 | end 14 | 15 | def set(n) do 16 | GenServer.cast(@server, {:set, n}) 17 | end 18 | 19 | def init([]) do 20 | :ets.new(@table, [:named_table, :public]) 21 | :ets.insert(@table, {:counter, 0}) 22 | {:ok, 0} 23 | end 24 | 25 | def handle_call(:count_down, _from, state) do 26 | [{:counter, n}] = :ets.lookup(@table, :counter) 27 | case n do 28 | 0 -> :ok 29 | _ -> 30 | :ets.insert(@table, {:counter, n-1}) 31 | GenServer.call(@server, :count_down, :infinity) 32 | end 33 | {:reply, 0, state} 34 | end 35 | 36 | def handle_cast({:set, n}, state) do 37 | :ets.insert(@table, {:counter, n}) 38 | {:noreply, state} 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/lib/ping_pong.ex: -------------------------------------------------------------------------------- 1 | defmodule PingPong do 2 | 3 | def ping do 4 | receive do 5 | :pong -> :ok 6 | end 7 | end 8 | 9 | def pong(ping_pid) do 10 | send(ping_pid, :pong) 11 | receive do 12 | :ping -> :ok 13 | end 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/lib/ping_pong_reg.ex: -------------------------------------------------------------------------------- 1 | defmodule PingPongReg do 2 | 3 | def start_ping do 4 | start_and_reg_proc(:ping, ping) 5 | end 6 | 7 | def start_pong do 8 | start_and_reg_proc(:pong, pong) 9 | end 10 | 11 | def start_and_reg_proc(name, fun) when is_atom(name) do 12 | case Process.whereis(name) do 13 | nil -> 14 | pid = spawn(name, fun, []) 15 | Process.register(name, pid) 16 | {:ok, pid} 17 | pid -> 18 | {:error, :already_started} 19 | end 20 | end 21 | 22 | def ping do 23 | receive do 24 | :pong -> 25 | IO.puts "pong" 26 | send(:pong, :ping) 27 | ping 28 | :stop -> 29 | :ok 30 | end 31 | end 32 | 33 | def pong do 34 | receive do 35 | :ping -> 36 | IO.puts "ping" 37 | send(:ping, :pong) 38 | pong 39 | :stop -> 40 | :ok 41 | end 42 | end 43 | 44 | end 45 | -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/lib/reg_server.ex: -------------------------------------------------------------------------------- 1 | defmodule RegServer do 2 | @reg_name __MODULE__ 3 | @reg_request :reg_request 4 | @reg_reply :reg_reply 5 | 6 | def start do 7 | pid = spawn(fn -> loop end) 8 | Process.register(pid, @reg_name) 9 | :ok 10 | end 11 | 12 | def ping do 13 | request(:ping) 14 | end 15 | 16 | def stop do 17 | request(:stop) 18 | end 19 | 20 | def loop do 21 | receive do 22 | {@reg_request, target, :ping} -> 23 | reply(target, :pong) 24 | loop 25 | 26 | {@reg_request, target, :stop} -> 27 | Process.unregister(@reg_name) 28 | reply(target, :ok) 29 | end 30 | end 31 | 32 | defp request(request) do 33 | case Process.whereis(@reg_name) do 34 | nil -> 35 | :server_down 36 | 37 | pid -> 38 | ref = Process.monitor(pid) 39 | send(pid, {@reg_request, self, request}) 40 | receive do 41 | {@reg_reply, reply} -> 42 | Process.demonitor(ref, [:flush]) 43 | reply 44 | 45 | {:DOWN, ^ref, :process, ^pid, _reason} -> 46 | :server_down 47 | 48 | end 49 | end 50 | end 51 | 52 | defp reply(target, reply) do 53 | send(target, {@reg_reply, reply}) 54 | end 55 | 56 | end 57 | -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/lib/spawn_reg.ex: -------------------------------------------------------------------------------- 1 | defmodule SpawnReg do 2 | 3 | @name __MODULE__ 4 | 5 | def start do 6 | case Process.whereis(@name) do 7 | nil -> 8 | pid = spawn(fn -> loop end) 9 | Process.register(pid, @name) 10 | :ok 11 | _ -> 12 | :already_started 13 | end 14 | end 15 | 16 | def loop do 17 | receive do 18 | :stop -> 19 | :ok 20 | _ -> 21 | loop 22 | end 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/lib/stacky.ex: -------------------------------------------------------------------------------- 1 | defmodule Stacky do 2 | use GenServer 3 | require Integer 4 | 5 | @name __MODULE__ 6 | 7 | def start_link do 8 | GenServer.start_link(__MODULE__, :ok, name: @name) 9 | end 10 | 11 | def add(item) do 12 | GenServer.call(@name, {:add, item}) 13 | end 14 | 15 | def tag(item) do 16 | GenServer.call(@name, {:tag, item}) 17 | end 18 | 19 | def stop do 20 | GenServer.call(@name, :stop) 21 | end 22 | 23 | def init(:ok) do 24 | {:ok, []} 25 | end 26 | 27 | def handle_call({:add, item}, _from, state) do 28 | new_state = [item|state] 29 | {:reply, {:ok, new_state}, new_state} 30 | end 31 | 32 | def handle_call({:tag, item}, _from, state) when Integer.is_even(item) do 33 | new_state = [{:even, item} |state] 34 | {:reply, {:ok, new_state}, new_state} 35 | end 36 | 37 | def handle_call({:tag, item}, _from, state) when Integer.is_odd(item) do 38 | new_state = [{:odd, item} |state] 39 | {:reply, {:ok, new_state}, new_state} 40 | end 41 | 42 | def handle_call(:stop, _from, state) do 43 | {:stop, :normal, state} 44 | end 45 | 46 | def terminate(_reason, _state) do 47 | :ok 48 | end 49 | 50 | end 51 | -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ConcuerrorPlayground.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :concuerror_playground, 6 | version: "0.0.1", 7 | elixir: "~> 1.2-rc", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | elixirc_paths: elixirc_paths(Mix.env), 11 | test_pattern: "*_test.ex*", 12 | warn_test_pattern: nil, 13 | deps: deps] 14 | end 15 | 16 | def application do 17 | [applications: [:logger]] 18 | end 19 | 20 | defp deps do 21 | [] 22 | end 23 | 24 | defp elixirc_paths(:test), do: ["lib", "test/concurrency"] 25 | defp elixirc_paths(_), do: ["lib"] 26 | end 27 | -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/ping_pong.dot: -------------------------------------------------------------------------------- 1 | digraph { 2 | graph [ranksep=0.3] 3 | node [shape=box,width=7,fontname=Monospace] 4 | init [label="Initial"]; 5 | subgraph { 6 | "#Ref<0.0.1.90>" [label=" 1: P: P.1 = erlang:spawn(erlang, apply, [...])\l"]; 7 | "init" -> "#Ref<0.0.1.90>"[weight=1000]; 8 | "#Ref<0.0.1.111>" [label=" 2: P: P.2 = erlang:spawn(erlang, apply, [...])\l"]; 9 | "#Ref<0.0.1.90>" -> "#Ref<0.0.1.111>"[weight=1000]; 10 | "#Ref<0.0.1.115>" [label=" 3: P: exits normally\l",color=lime,penwidth=5]; 11 | "#Ref<0.0.1.111>" -> "#Ref<0.0.1.115>"[weight=1000]; 12 | "#Ref<0.0.1.117>" [label=" 4: P.2: pong = erlang:send(P.1, pong)\l"]; 13 | "#Ref<0.0.1.115>" -> "#Ref<0.0.1.117>"[weight=1000]; 14 | "#Ref<0.0.1.120>" [label=" 5: Message (pong) from P.2 reaches P.1\l"]; 15 | "#Ref<0.0.1.117>" -> "#Ref<0.0.1.120>"[weight=1000]; 16 | "#Ref<0.0.1.122>" [label=" 6: P.1: receives message (pong)\l"]; 17 | "#Ref<0.0.1.120>" -> "#Ref<0.0.1.122>"[weight=1000]; 18 | "#Ref<0.0.1.124>" [label=" 7: P.1: exits normally\l",color=lime,penwidth=5]; 19 | "#Ref<0.0.1.122>" -> "#Ref<0.0.1.124>"[weight=1000]; 20 | "#Ref<0.0.1.153>" [label="1: Error ([P.2] blocked)",style=filled,fillcolor=red]; 21 | "#Ref<0.0.1.124>" -> "#Ref<0.0.1.153>"[weight=1000]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/ping_pong.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjamintanweihao/the-little-elixir-otp-guidebook-code/78b372fec5bc6f93203707c72a18dabe0fbaca3d/chapter_11/concuerror_playground/ping_pong.png -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/ping_pong_reg.dot: -------------------------------------------------------------------------------- 1 | digraph { 2 | graph [ranksep=0.3] 3 | node [shape=box,width=7,fontname=Monospace] 4 | init [label="Initial"]; 5 | subgraph { 6 | "#Ref<0.0.1.111>" [label="1: Error ([P] blocked)",style=filled,fillcolor=red]; 7 | "init" -> "#Ref<0.0.1.111>"[weight=1000]; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/ping_pong_reg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjamintanweihao/the-little-elixir-otp-guidebook-code/78b372fec5bc6f93203707c72a18dabe0fbaca3d/chapter_11/concuerror_playground/ping_pong_reg.png -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/spawn_link.dot: -------------------------------------------------------------------------------- 1 | digraph { 2 | graph [ranksep=0.3] 3 | node [shape=box,width=7,fontname=Monospace] 4 | init [label="Initial"]; 5 | subgraph { 6 | "#Ref<0.0.1.90>" [label=" 1: P: P.1 = erlang:spawn(erlang, apply, [...])\l"]; 7 | "init" -> "#Ref<0.0.1.90>"[weight=1000]; 8 | "#Ref<0.0.1.111>" [label=" 2: P: true = erlang:link(P.1)\l"]; 9 | "#Ref<0.0.1.90>" -> "#Ref<0.0.1.111>"[weight=1000]; 10 | "#Ref<0.0.1.115>" [label=" 3: P: exits normally\l",color=lime,penwidth=5]; 11 | "#Ref<0.0.1.111>" -> "#Ref<0.0.1.115>"[weight=1000]; 12 | "#Ref<0.0.1.117>" [label=" 4: P: true = erlang:exit(P.1, normal)\l"]; 13 | "#Ref<0.0.1.115>" -> "#Ref<0.0.1.117>"[weight=1000]; 14 | "#Ref<0.0.1.120>" [label=" 5: Exit signal (normal) from P reaches P.1\l"]; 15 | "#Ref<0.0.1.117>" -> "#Ref<0.0.1.120>"[weight=1000]; 16 | "#Ref<0.0.1.122>" [label=" 6: P.1: receive timeout expired after 5000 ms\l"]; 17 | "#Ref<0.0.1.120>" -> "#Ref<0.0.1.122>"[weight=1000]; 18 | "#Ref<0.0.1.124>" [label=" 7: P.1: exits normally\l",color=lime,penwidth=5]; 19 | "#Ref<0.0.1.122>" -> "#Ref<0.0.1.124>"[weight=1000]; 20 | "#Ref<0.0.1.126>" [label=" 8: P.1: true = erlang:exit(P, normal)\l"]; 21 | "#Ref<0.0.1.124>" -> "#Ref<0.0.1.126>"[weight=1000]; 22 | "#Ref<0.0.1.129>" [label=" 9: Exit signal (normal) from P.1 reaches P\l"]; 23 | "#Ref<0.0.1.126>" -> "#Ref<0.0.1.129>"[weight=1000]; 24 | "#Ref<0.0.1.111>":e -> "#Ref<0.0.1.124>":e[constraint=false, color=red, dir=back, penwidth=3, style=dashed]; 25 | "#Ref<0.0.1.161>" [label="1: Ok",style=filled,fillcolor=lime]; 26 | "#Ref<0.0.1.129>" -> "#Ref<0.0.1.161>"[weight=1000]; 27 | } 28 | subgraph{ 29 | "#Ref<0.0.1.145>" [label=" 2: P.1: receive timeout expired after 5000 ms\l"]; 30 | "#Ref<0.0.1.90>" -> "#Ref<0.0.1.145>"[style=invis,weight=1]; 31 | "#Ref<0.0.1.111>" -> "#Ref<0.0.1.145>"[constraint=false]; 32 | "#Ref<0.0.1.148>" [label=" 3: P.1: exits normally\l",color=lime,penwidth=5]; 33 | "#Ref<0.0.1.145>" -> "#Ref<0.0.1.148>"[weight=1000]; 34 | "#Ref<0.0.1.150>" [label=" 4: P: Exception noproc raised by: erlang:link(P.1)\l",color=orange,penwidth=5]; 35 | "#Ref<0.0.1.148>" -> "#Ref<0.0.1.150>"[weight=1000]; 36 | "#Ref<0.0.1.153>" [label=" 5: P: exits abnormally ({...})\l",color=red,penwidth=5]; 37 | "#Ref<0.0.1.150>" -> "#Ref<0.0.1.153>"[weight=1000]; 38 | "#Ref<0.0.1.181>" [label="2: Error",style=filled,fillcolor=red]; 39 | "#Ref<0.0.1.153>" -> "#Ref<0.0.1.181>"[weight=1000]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/spawn_link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjamintanweihao/the-little-elixir-otp-guidebook-code/78b372fec5bc6f93203707c72a18dabe0fbaca3d/chapter_11/concuerror_playground/spawn_link.png -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/spawn_reg.dot: -------------------------------------------------------------------------------- 1 | digraph { 2 | graph [ranksep=0.3] 3 | node [shape=box,width=7,fontname=Monospace] 4 | init [label="Initial"]; 5 | subgraph { 6 | "#Ref<0.0.1.90>" [label=" 1: P: P.1 = erlang:spawn(erlang, apply, [...])\l"]; 7 | "init" -> "#Ref<0.0.1.90>"[weight=1000]; 8 | "#Ref<0.0.1.117>" [label=" 2: P: Exception badarg raised by: erlang:send('Elixir.SpawnReg', stop)\l",color=orange,penwidth=5]; 9 | "#Ref<0.0.1.90>" -> "#Ref<0.0.1.117>"[weight=1000]; 10 | "#Ref<0.0.1.121>" [label=" 3: P: exits abnormally ({...})\l",color=red,penwidth=5]; 11 | "#Ref<0.0.1.117>" -> "#Ref<0.0.1.121>"[weight=1000]; 12 | "#Ref<0.0.1.123>" [label=" 4: P.1: undefined = erlang:whereis('Elixir.SpawnReg')\l"]; 13 | "#Ref<0.0.1.121>" -> "#Ref<0.0.1.123>"[weight=1000]; 14 | "#Ref<0.0.1.125>" [label=" 5: P.1: P.1.1 = erlang:spawn(erlang, apply, [...])\l"]; 15 | "#Ref<0.0.1.123>" -> "#Ref<0.0.1.125>"[weight=1000]; 16 | "#Ref<0.0.1.127>" [label=" 6: P.1: true = erlang:register('Elixir.SpawnReg', P.1.1)\l"]; 17 | "#Ref<0.0.1.125>" -> "#Ref<0.0.1.127>"[weight=1000]; 18 | "#Ref<0.0.1.129>" [label=" 7: P.1: exits normally\l",color=lime,penwidth=5]; 19 | "#Ref<0.0.1.127>" -> "#Ref<0.0.1.129>"[weight=1000]; 20 | "#Ref<0.0.1.117>":e -> "#Ref<0.0.1.127>":e[constraint=false, color=red, dir=back, penwidth=3, style=dashed]; 21 | "#Ref<0.0.1.161>" [label="1: Error",style=filled,fillcolor=red]; 22 | "#Ref<0.0.1.129>" -> "#Ref<0.0.1.161>"[weight=1000]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/spawn_reg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjamintanweihao/the-little-elixir-otp-guidebook-code/78b372fec5bc6f93203707c72a18dabe0fbaca3d/chapter_11/concuerror_playground/spawn_reg.png -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/stacky.dot: -------------------------------------------------------------------------------- 1 | digraph { 2 | graph [ranksep=0.3] 3 | node [shape=box,width=7,fontname=Monospace] 4 | init [label="Initial"]; 5 | subgraph { 6 | "#Ref<0.0.1.90>" [label=" 1: P: undefined = erlang:whereis('Elixir.Stacky')\l"]; 7 | "init" -> "#Ref<0.0.1.90>"[weight=1000]; 8 | "#Ref<0.0.1.128>" [label=" 2: P: [] = erlang:process_info(P, registered_name)\l"]; 9 | "#Ref<0.0.1.90>" -> "#Ref<0.0.1.128>"[weight=1000]; 10 | "#Ref<0.0.1.140>" [label=" 3: P: P.1 = erlang:spawn_opt({...})\l"]; 11 | "#Ref<0.0.1.128>" -> "#Ref<0.0.1.140>"[weight=1000]; 12 | "#Ref<0.0.1.151>" [label=" 4: P.1: undefined = erlang:put('$ancestors', [...])\l"]; 13 | "#Ref<0.0.1.140>" -> "#Ref<0.0.1.151>"[weight=1000]; 14 | "#Ref<0.0.1.154>" [label=" 5: P.1: undefined = erlang:put('$initial_call', {...})\l"]; 15 | "#Ref<0.0.1.151>" -> "#Ref<0.0.1.154>"[weight=1000]; 16 | "#Ref<0.0.1.157>" [label=" 6: P.1: true = erlang:register('Elixir.Stacky', P.1)\l"]; 17 | "#Ref<0.0.1.154>" -> "#Ref<0.0.1.157>"[weight=1000]; 18 | "#Ref<0.0.1.160>" [label=" 7: P.1: {...} = P ! {...}\l"]; 19 | "#Ref<0.0.1.157>" -> "#Ref<0.0.1.160>"[weight=1000]; 20 | "#Ref<0.0.1.173>" [label=" 8: Message ({...}) from P.1 reaches P\l"]; 21 | "#Ref<0.0.1.160>" -> "#Ref<0.0.1.173>"[weight=1000]; 22 | "#Ref<0.0.1.177>" [label=" 9: P: receives message ({...})\l"]; 23 | "#Ref<0.0.1.173>" -> "#Ref<0.0.1.177>"[weight=1000]; 24 | "#Ref<0.0.1.180>" [label=" 10: P: P.1 = erlang:whereis('Elixir.Stacky')\l"]; 25 | "#Ref<0.0.1.177>" -> "#Ref<0.0.1.180>"[weight=1000]; 26 | "#Ref<0.0.1.182>" [label=" 11: P: #Ref<0.0.1.185> = erlang:monitor(process, P.1)\l"]; 27 | "#Ref<0.0.1.180>" -> "#Ref<0.0.1.182>"[weight=1000]; 28 | "#Ref<0.0.1.186>" [label=" 12: P: {...} = erlang:send(P.1, {...}, [...])\l"]; 29 | "#Ref<0.0.1.182>" -> "#Ref<0.0.1.186>"[weight=1000]; 30 | "#Ref<0.0.1.190>" [label=" 13: Message ({...}) from P reaches P.1\l"]; 31 | "#Ref<0.0.1.186>" -> "#Ref<0.0.1.190>"[weight=1000]; 32 | "#Ref<0.0.1.194>" [label=" 14: P.1: receives message ({...})\l"]; 33 | "#Ref<0.0.1.190>" -> "#Ref<0.0.1.194>"[weight=1000]; 34 | "#Ref<0.0.1.197>" [label=" 15: P.1: {...} = P ! {...}\l"]; 35 | "#Ref<0.0.1.194>" -> "#Ref<0.0.1.197>"[weight=1000]; 36 | "#Ref<0.0.1.201>" [label=" 16: Message ({...}) from P.1 reaches P\l"]; 37 | "#Ref<0.0.1.197>" -> "#Ref<0.0.1.201>"[weight=1000]; 38 | "#Ref<0.0.1.205>" [label=" 17: P: receives message ({...})\l"]; 39 | "#Ref<0.0.1.201>" -> "#Ref<0.0.1.205>"[weight=1000]; 40 | "#Ref<0.0.1.208>" [label=" 18: P: true = erlang:demonitor(#Ref<0.0.1.185>, [...])\l"]; 41 | "#Ref<0.0.1.205>" -> "#Ref<0.0.1.208>"[weight=1000]; 42 | "#Ref<0.0.1.211>" [label=" 19: P: P.1 = erlang:whereis('Elixir.Stacky')\l"]; 43 | "#Ref<0.0.1.208>" -> "#Ref<0.0.1.211>"[weight=1000]; 44 | "#Ref<0.0.1.214>" [label=" 20: P: #Ref<0.0.1.217> = erlang:monitor(process, P.1)\l"]; 45 | "#Ref<0.0.1.211>" -> "#Ref<0.0.1.214>"[weight=1000]; 46 | "#Ref<0.0.1.218>" [label=" 21: P: {...} = erlang:send(P.1, {...}, [...])\l"]; 47 | "#Ref<0.0.1.214>" -> "#Ref<0.0.1.218>"[weight=1000]; 48 | "#Ref<0.0.1.222>" [label=" 22: Message ({...}) from P reaches P.1\l"]; 49 | "#Ref<0.0.1.218>" -> "#Ref<0.0.1.222>"[weight=1000]; 50 | "#Ref<0.0.1.225>" [label=" 23: P.1: receives message ({...})\l"]; 51 | "#Ref<0.0.1.222>" -> "#Ref<0.0.1.225>"[weight=1000]; 52 | "#Ref<0.0.1.229>" [label=" 24: P.1: exits normally\l",color=lime,penwidth=5]; 53 | "#Ref<0.0.1.225>" -> "#Ref<0.0.1.229>"[weight=1000]; 54 | "#Ref<0.0.1.232>" [label=" 25: P.1: true = erlang:exit(P, normal)\l"]; 55 | "#Ref<0.0.1.229>" -> "#Ref<0.0.1.232>"[weight=1000]; 56 | "#Ref<0.0.1.236>" [label=" 26: P.1: {...} = erlang:send(P, {...})\l"]; 57 | "#Ref<0.0.1.232>" -> "#Ref<0.0.1.236>"[weight=1000]; 58 | "#Ref<0.0.1.240>" [label=" 27: Exit signal (normal) from P.1 reaches P\l"]; 59 | "#Ref<0.0.1.236>" -> "#Ref<0.0.1.240>"[weight=1000]; 60 | "#Ref<0.0.1.243>" [label=" 28: Message ({...}) from P.1 reaches P\l"]; 61 | "#Ref<0.0.1.240>" -> "#Ref<0.0.1.243>"[weight=1000]; 62 | "#Ref<0.0.1.247>" [label=" 29: P: receives message ({...})\l"]; 63 | "#Ref<0.0.1.243>" -> "#Ref<0.0.1.247>"[weight=1000]; 64 | "#Ref<0.0.1.250>" [label=" 30: P: exits abnormally ({...})\l",color=red,penwidth=5]; 65 | "#Ref<0.0.1.247>" -> "#Ref<0.0.1.250>"[weight=1000]; 66 | "#Ref<0.0.1.253>" [label=" 31: P: true = erlang:exit(P.1, {...})\l"]; 67 | "#Ref<0.0.1.250>" -> "#Ref<0.0.1.253>"[weight=1000]; 68 | "#Ref<0.0.1.257>" [label=" 32: Exit signal ({...}) from P reaches P.1\l"]; 69 | "#Ref<0.0.1.253>" -> "#Ref<0.0.1.257>"[weight=1000]; 70 | "#Ref<0.0.1.292>" [label="1: Ok",style=filled,fillcolor=lime]; 71 | "#Ref<0.0.1.257>" -> "#Ref<0.0.1.292>"[weight=1000]; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/stacky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjamintanweihao/the-little-elixir-otp-guidebook-code/78b372fec5bc6f93203707c72a18dabe0fbaca3d/chapter_11/concuerror_playground/stacky.png -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/test/concurrency/counter_server_test.ex: -------------------------------------------------------------------------------- 1 | defmodule CounterServer.ConcurrencyTest do 2 | 3 | def test do 4 | {:ok, _pid} = CounterServer.start_link 5 | CounterServer.set(10) 6 | CounterServer.count_down 7 | end 8 | 9 | end 10 | -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/test/concurrency/ping_pong_reg_test.ex: -------------------------------------------------------------------------------- 1 | Code.require_file "../test_helper.exs", __DIR__ 2 | 3 | defmodule PingPongReg.ConcurrencyTest do 4 | use ExUnit.Case 5 | import PingPongReg 6 | 7 | def test do 8 | 9 | {:ok, ping_pid} = start_ping 10 | {:ok, pong_pid} = start_pong 11 | 12 | # Kickstart the ping/pong session 13 | send(:ping, :pong) 14 | send(:pong, :stop) 15 | send(:ping, :stop) 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/test/concurrency/ping_pong_test.ex: -------------------------------------------------------------------------------- 1 | Code.require_file "../test_helper.exs", __DIR__ 2 | 3 | defmodule PingPong.ConcurrencyTest do 4 | use ExUnit.Case 5 | 6 | def test do 7 | ping_pid = spawn(fn -> PingPong.ping end) 8 | spawn(fn -> PingPong.pong(ping_pid) end) 9 | end 10 | 11 | end 12 | -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/test/concurrency/reg_server_test.ex: -------------------------------------------------------------------------------- 1 | Code.require_file "../test_helper.exs", __DIR__ 2 | 3 | defmodule RegServer.ConcurrencyTest do 4 | use ExUnit.Case 5 | 6 | test "start stop" do 7 | assert :ok = RegServer.start 8 | assert :ok = RegServer.stop 9 | end 10 | 11 | test "ping" do 12 | RegServer.start 13 | assert :pong = RegServer.ping 14 | assert :pong = RegServer.ping 15 | RegServer.stop 16 | end 17 | 18 | test "multiple stops" do 19 | RegServer.start 20 | assert :ok = RegServer.stop 21 | assert :server_down = RegServer.stop 22 | end 23 | 24 | test "multiple concurrent stops" do 25 | RegServer.start 26 | me = self 27 | spawn(fn -> send(me, RegServer.stop) end) 28 | spawn(fn -> send(me, RegServer.stop) end) 29 | 30 | assert [:ok, :server_down] = receive_two 31 | end 32 | 33 | defp receive_two do 34 | receive do 35 | result_1 -> 36 | receive do 37 | result_2 -> 38 | [result_1, result_2] |> Enum.sort 39 | end 40 | end 41 | end 42 | 43 | # This is for Concuerror, which defaults to test/0 44 | # NOTE: run only one! 45 | def test do 46 | # apply(__MODULE__, :"test start stop", [""]) 47 | # apply(__MODULE__, :"test ping", [""]) 48 | # apply(__MODULE__, :"test multiple stops", [""]) 49 | apply(__MODULE__, :"test multiple concurrent stops", [""]) 50 | end 51 | 52 | end 53 | -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/test/concurrency/spawn_link_test.ex: -------------------------------------------------------------------------------- 1 | Code.require_file "../test_helper.exs", __DIR__ 2 | 3 | defmodule SpawnLink.ConcurrencyTest do 4 | 5 | def test do 6 | # This will call a crash! 7 | Process.link(spawn(fn -> 8 | :timer.sleep(5000) 9 | end)) 10 | 11 | # This will not! 12 | # spawn_link(fn -> 13 | # :timer.sleep(5000) 14 | # end) 15 | 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/test/concurrency/spawn_reg.ex: -------------------------------------------------------------------------------- 1 | Code.require_file "../test_helper.exs", __DIR__ 2 | 3 | defmodule SpawnReg.ConcurrencyTest do 4 | 5 | def test do 6 | spawn(fn -> SpawnReg.start end) 7 | send(SpawnReg, :stop) 8 | end 9 | 10 | end 11 | -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/test/concurrency/spawn_reg_test.ex: -------------------------------------------------------------------------------- 1 | Code.require_file "../test_helper.exs", __DIR__ 2 | 3 | defmodule SpawnReg.ConcurrencyTest do 4 | use ExUnit.Case 5 | 6 | def test do 7 | spawn(fn -> SpawnReg.start end) 8 | send(SpawnReg, :stop) 9 | 10 | # The race condition here happens because 11 | # the process might not complete setting name up yet. 12 | # Therefore, send/2 might fail if `:name` 13 | # is not registered yet 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/test/concurrency/stacky_test.ex: -------------------------------------------------------------------------------- 1 | Code.require_file "../test_helper.exs", __DIR__ 2 | 3 | defmodule Stacky.ConcurrencyTest do 4 | 5 | def test do 6 | {:ok, _pid} = Stacky.start_link 7 | Stacky.tag(1) 8 | Stacky.stop 9 | :ok 10 | end 11 | 12 | end 13 | 14 | 15 | -------------------------------------------------------------------------------- /chapter_11/concuerror_playground/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /chapter_11/quickcheck_playground/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | *.eqc 7 | .eqc-info 8 | -------------------------------------------------------------------------------- /chapter_11/quickcheck_playground/.vimrc: -------------------------------------------------------------------------------- 1 | " make sure the following 2 lines are included in ~/.vimrc 2 | " set exrc 3 | " set secure 4 | 5 | map t :!mix test % 6 | map c :!mix compile && mix dialyzer 7 | -------------------------------------------------------------------------------- /chapter_11/quickcheck_playground/README.md: -------------------------------------------------------------------------------- 1 | # Quickcheck Playground 2 | 3 | This project uses QuickCheck Mini from Quviq. 4 | 5 | 1. Head over to [QuviQ](http://www.quviq.com/downloads/) and download the _FREE_ version of QuickCheck (Mini). 6 | 2. Unzip the folder, and `cd` into it. 7 | 3. Run `iex`. 8 | 4. Run `:eqc_install.install()` to install QuickCheck Mini. 9 | 5. Clone this repo, and run `mix deps.get` 10 | 6. Run `mix tests` to run the tests. 11 | -------------------------------------------------------------------------------- /chapter_11/quickcheck_playground/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :quickcheck_playground, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:quickcheck_playground, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /chapter_11/quickcheck_playground/lib/quickcheck_playground.ex: -------------------------------------------------------------------------------- 1 | defmodule QuickcheckPlayground do 2 | end 3 | -------------------------------------------------------------------------------- /chapter_11/quickcheck_playground/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule QuickcheckPlayground.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :quickcheck_playground, 6 | version: "0.0.1", 7 | elixir: "~> 1.2-rc", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | test_pattern: "*_{test,eqc}.exs", 11 | deps: deps] 12 | end 13 | 14 | def application do 15 | [applications: [:logger]] 16 | end 17 | 18 | defp deps do 19 | [ 20 | {:eqc_ex, "~> 1.2.4"} 21 | ] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /chapter_11/quickcheck_playground/mix.lock: -------------------------------------------------------------------------------- 1 | %{"eqc_ex": {:hex, :eqc_ex, "1.2.4"}, 2 | "excheck": {:hex, :excheck, "0.3.2"}, 3 | "triq": {:git, "https://github.com/krestenkrab/triq.git", "c7306b8eaea133d52140cb828817efb5e50a3d52", []}} 4 | -------------------------------------------------------------------------------- /chapter_11/quickcheck_playground/test/list_eqc.exs: -------------------------------------------------------------------------------- 1 | defmodule ListEQC do 2 | use ExUnit.Case 3 | use EQC.ExUnit 4 | 5 | @tag numtests: 100 6 | property "Deleting from a list" do 7 | forall list <- ulist(int) do 8 | implies list != [] do 9 | forall item <- elements(list) do 10 | ensure not item in List.delete(list, item) == true 11 | end 12 | end 13 | end 14 | end 15 | 16 | property "Misconception of deleting from a list" do 17 | :eqc.fails( 18 | forall list <- list(int) do 19 | implies list != [] do 20 | forall item <- elements(list) do 21 | ensure not item in List.delete(list, item) == true 22 | end 23 | end 24 | end 25 | ) 26 | end 27 | 28 | property "Deleting an element not in a list leaves the list unchanged" do 29 | forall list <- list(int) do 30 | forall item <- int do 31 | implies not item in list do 32 | ensure list == List.delete(list, item) 33 | end 34 | end 35 | end 36 | end 37 | 38 | property "sorting works" do 39 | forall l <- list(int) do 40 | ensure l |> Enum.sort |> is_sorted == true 41 | end 42 | end 43 | 44 | 45 | # NOTE: Testing properties of reverse 46 | 47 | property "reverse is recursive" do 48 | forall l <- non_empty(list(char)) do 49 | equal Enum.reverse(l), Enum.reverse(tl(l)) ++ [hd(l)] 50 | end 51 | end 52 | 53 | property "reverse is distributive" do 54 | forall {l1, l2} <- {list(char), list(char)} do 55 | ensure Enum.reverse(l1 ++ l2) == Enum.reverse(l2) ++ Enum.reverse(l1) 56 | end 57 | end 58 | 59 | property "reverse is idempotent" do 60 | forall l <- list(char) do 61 | ensure l |> Enum.reverse |> Enum.reverse == l 62 | end 63 | end 64 | 65 | property "reverse is equivalent to the Erlang version" do 66 | forall l <- list(oneof [real, int]) do 67 | ensure Enum.reverse(l) == :lists.reverse(l) 68 | end 69 | end 70 | 71 | def is_sorted([]), do: true 72 | 73 | def is_sorted(list) do 74 | list 75 | |> Enum.zip(tl(list)) 76 | |> Enum.all?(fn {x, y} -> x <= y end) 77 | end 78 | 79 | def equal(x, y) do 80 | when_fail(IO.puts("FAILED ☛ #{inspect(x)} != #{inspect(y)}")) do 81 | x == y 82 | end 83 | end 84 | 85 | # Custom generator to generate unique lists 86 | def ulist(item) do 87 | let l <- list(item) do 88 | l |> Enum.sort |> Enum.uniq 89 | end 90 | end 91 | 92 | end 93 | -------------------------------------------------------------------------------- /chapter_11/quickcheck_playground/test/map_eqc.exs: -------------------------------------------------------------------------------- 1 | defmodule MapEQC do 2 | use ExUnit.Case 3 | use EQC.ExUnit 4 | 5 | # NOTE: Strange that this should fail but doesn't. 6 | property "keys are unique (take 1)" do 7 | forall m <- map_1 do 8 | no_duplicates(Map.keys(m)) 9 | end 10 | end 11 | 12 | property "keys are unique (take 2)" do 13 | forall m <- map_2 do 14 | no_duplicates(Map.keys(:eqc_symbolic.eval(m))) 15 | end 16 | end 17 | 18 | property "storing keys and values" do 19 | forall {k, v, m} <- {key, val, map_2} do 20 | map = :eqc_symbolic.eval(m) 21 | lists_equal(model(Map.put(map, k, v)), model_store(k, v, model(map))) 22 | end 23 | end 24 | 25 | property "merging maps is *not* commutative" do 26 | forall {m1, m2} <- {map_2, map_2} do 27 | map_1 = :eqc_symbolic.eval(m1) 28 | map_2 = :eqc_symbolic.eval(m2) 29 | 30 | # NOTE: This will not work, since keys can be overriden! 31 | # Cool that QC finds this out after ~ 79 tests 32 | :eqc.fails( 33 | ensure Map.merge(map_1, map_2) == Map.merge(map_2, map_1) 34 | ) 35 | end 36 | end 37 | 38 | property "merging maps retains keys" do 39 | forall {m1, m2} <- {map_2, map_2} do 40 | map_1 = :eqc_symbolic.eval(m1) 41 | map_2 = :eqc_symbolic.eval(m2) 42 | 43 | left_keys = Map.merge(map_1, map_2) |> Map.keys 44 | right_keys = Map.merge(map_2, map_1) |> Map.keys 45 | 46 | equal(left_keys, right_keys) 47 | end 48 | end 49 | 50 | # First version of map generator 51 | # NOTE: there's a recursive call to map_1(). We need to 52 | # use the `lazy` macro here. 53 | def map_1 do 54 | map_gen = lazy do 55 | let {k, v, m} <- {key, val, map_1} do 56 | Map.put(m, k, v) 57 | end 58 | end 59 | 60 | oneof [Map.new, map_gen] 61 | end 62 | 63 | # NOTE: Make sure that the order is right! 64 | # {:call, Map, :put, [key, val, map_2]}] will *not* work! 65 | def map_2 do 66 | lazy do 67 | oneof [{:call, Map, :new, []}, 68 | {:call, Map, :put, [map_2, key, val]}] 69 | end 70 | end 71 | 72 | def no_duplicates(elems) do 73 | left = elems |> Enum.sort 74 | right = elems |> Enum.uniq |> Enum.sort 75 | # equal(:lists.sort(elems), :lists.usort(elems)) 76 | equal(left, right) 77 | end 78 | 79 | def key do 80 | oneof [int, real, atom] 81 | end 82 | 83 | def val do 84 | key 85 | end 86 | 87 | def atom do 88 | elements [:a, :b, :c, true, false, :ok] 89 | end 90 | 91 | def model(map) do 92 | Map.to_list(map) 93 | end 94 | 95 | def model_store(k, v, list) do 96 | case find_index_with_key(k, list) do 97 | {:match, index} -> 98 | List.replace_at(list, index, {k, v}) 99 | _ -> 100 | [{k, v} | list] 101 | end 102 | end 103 | 104 | def find_index_with_key(k, list) do 105 | case Enum.find_index(list, fn({x,_}) -> x == k end) do 106 | nil -> :nomatch 107 | index -> {:match, index} 108 | end 109 | end 110 | 111 | def equal(x, y) do 112 | when_fail(IO.puts("FAILED ☛ #{inspect(x)} != #{inspect(y)}")) do 113 | x == y 114 | end 115 | end 116 | 117 | def lists_equal(x, y) do 118 | equal(Enum.sort(x), Enum.sort(y)) 119 | end 120 | 121 | end 122 | -------------------------------------------------------------------------------- /chapter_11/quickcheck_playground/test/numbers_eqc.exs: -------------------------------------------------------------------------------- 1 | defmodule NumbersEQC do 2 | use ExUnit.Case 3 | use EQC.ExUnit 4 | 5 | property "the square of a real number is always positive" do 6 | forall x <- real do 7 | ensure x*x >= 0 8 | end 9 | end 10 | 11 | property "the square of the square root of a real number is the original number" do 12 | forall x <- pos_real do 13 | sq_root = :math.sqrt(x) 14 | ensure sq_root * sq_root - x < 0.000000000001 15 | end 16 | end 17 | 18 | def pos_real do 19 | let r <- real do 20 | :erlang.abs(r) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /chapter_11/quickcheck_playground/test/others_eqc.exs: -------------------------------------------------------------------------------- 1 | defmodule OthersEQC do 2 | use ExUnit.Case 3 | use EQC.ExUnit 4 | 5 | property "encoding then decoding a binary gives back the same binary" do 6 | forall s <- binary do 7 | equal s |> Base.encode16 |> Base.decode16!, s 8 | end 9 | end 10 | 11 | def equal(x, y) do 12 | when_fail(IO.puts("FAILED ☛ #{inspect(x)} != #{inspect(y)}")) do 13 | x == y 14 | end 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /chapter_11/quickcheck_playground/test/string_eqc.exs: -------------------------------------------------------------------------------- 1 | defmodule StringEQC do 2 | use ExUnit.Case 3 | use EQC.ExUnit 4 | 5 | # NOTE: We will get deprecation warnings if `s1` is empty. 6 | property "starts with" do 7 | forall {s1, s2} <- {non_empty(list(char)), list(char)} do 8 | s1 = to_string(s1) 9 | s2 = to_string(s2) 10 | ensure String.starts_with?(s1 <> s2, s1) == true 11 | end 12 | end 13 | 14 | # NOTE: We will get deprecation warnings if `s2` is empty. 15 | property "ends with" do 16 | forall {s1, s2} <- {list(char), non_empty(list(char))} do 17 | s1 = to_string(s1) 18 | s2 = to_string(s2) 19 | ensure String.ends_with?(s1 <> s2, s2) == true 20 | end 21 | end 22 | 23 | # NOTE: We make use of Erlang's `:string.len` function to 24 | # test against Elixir's implementation 25 | property "length" do 26 | forall s <- list(char) do 27 | str = to_string(s) 28 | equal(String.length(str), :string.len(s)) 29 | end 30 | end 31 | 32 | property "slice" do 33 | forall {s1, s2} <- {list(char), list(char)} do 34 | s1 = to_string(s1) 35 | s2 = to_string(s2) 36 | equal(String.slice(s1 <> s2, String.length(s1), String.length(s2)), s2) 37 | end 38 | end 39 | 40 | # NOTE: We use collect here to point out that the distribution of the data is pretty crappy 41 | # property "splitting and joining a string with a delimiter yields back the original string" do 42 | # forall {s, d} <- {string, delimiter} do 43 | # collect string: s, delimiter: d, 44 | # in: 45 | # equal(String.split(s, d) |> join(d), s) 46 | # end 47 | # end 48 | 49 | # NOTE: We can increase the number of tests with this tag. 50 | # @tag numtests: 2000 51 | property "splitting and joining a string with a delimiter yields back the original string" do 52 | 53 | forall s <- long_string do 54 | # NOTE: We are using sublists here, since delimiters can contain more than 55 | # one character 56 | forall d <- non_empty(sublist(s)) do 57 | s = to_string(s) 58 | d = to_string(d) 59 | 60 | # NOTE: collect must only have the property in `in:`. in other words, only 61 | # one line. 62 | # collect string: s, delimiter: d, in: 63 | equal(String.split(s, d) |> join(d), s) 64 | end 65 | end 66 | 67 | end 68 | 69 | property "split string with commas" do 70 | forall s <- string_with_commas do 71 | s = to_string(s) 72 | equal(String.split(s, ",") |> join(","), s) 73 | end 74 | end 75 | 76 | def string do 77 | # NOTE: We can use `non_empty` instead of `implies s != []` 78 | # forall s <- non_empty(list(char)) do 79 | # implies s != [] do 80 | # ... 81 | # end 82 | # end 83 | non_empty(list(alpha)) 84 | end 85 | 86 | # NOTE: We can use this to construct a variable length string 87 | # that makes use of a vector 88 | def long_string do 89 | let l <- oneof(:lists.seq(10, 100)) do 90 | vector(l, alpha) 91 | end 92 | end 93 | 94 | def alpha do 95 | # oneof(:lists.seq(?a, ?z) ++ :lists.seq(?A, ?Z)) 96 | oneof(:lists.seq(?a, ?z)) 97 | end 98 | 99 | # NOTE: Woah, this is cool. We generate a string between 10 and 20 100 | # characters long, made of up lower cased letters and the 101 | # comma character, of different frequencies. 102 | def string_with_commas do 103 | let len <- choose(10, 20) do 104 | let string <- vector(len, 105 | frequency([{10, oneof(:lists.seq(?a, ?z))}, 106 | {2 , ?,}])) do 107 | string 108 | end 109 | end 110 | end 111 | 112 | def join(parts, delimiter) do 113 | parts |> Enum.intersperse([delimiter]) |> Enum.join 114 | end 115 | 116 | def equal(x, y) do 117 | when_fail(IO.puts("FAILED ☛ #{inspect(x)} != #{inspect(y)}")) do 118 | x == y 119 | end 120 | end 121 | 122 | end 123 | -------------------------------------------------------------------------------- /chapter_11/quickcheck_playground/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start 2 | -------------------------------------------------------------------------------- /chapter_2/2_1/length_converter.ex: -------------------------------------------------------------------------------- 1 | defmodule MeterToFootConverter do 2 | def convert(m) do 3 | m * 3.28084 4 | end 5 | end 6 | 7 | -------------------------------------------------------------------------------- /chapter_2/2_2/length_converter_2_4.ex: -------------------------------------------------------------------------------- 1 | defmodule MeterToLengthConverter do 2 | defmodule Feet do 3 | def convert(m) do 4 | m * 3.28084 5 | end 6 | end 7 | 8 | defmodule Inch do 9 | def convert(m) do 10 | m * 39.3701 11 | end 12 | end 13 | end 14 | 15 | -------------------------------------------------------------------------------- /chapter_2/2_2/length_converter_2_6.ex: -------------------------------------------------------------------------------- 1 | defmodule MeterToLengthConverter do 2 | def convert(:feet, m), do: m * 3.28084 3 | def convert(:inch, m), do: m * 39.3701 4 | def convert(:yard, m), do: m * 1.09361 5 | end 6 | 7 | -------------------------------------------------------------------------------- /chapter_2/2_3/length_converter.ex: -------------------------------------------------------------------------------- 1 | defmodule MeterToLengthConverter do 2 | def convert(:feet, m) when is_number(m), do: m * 3.28084 3 | def convert(:inch, m) when is_number(m), do: m * 39.3701 4 | def convert(:yard, m) when is_number(m), do: m * 1.09361 5 | end 6 | 7 | -------------------------------------------------------------------------------- /chapter_2/2_4/id3.ex: -------------------------------------------------------------------------------- 1 | defmodule ID3Parser do 2 | 3 | def parse(file_name) do 4 | case File.read(file_name) do 5 | 6 | {:ok, mp3} -> 7 | mp3_byte_size = byte_size(mp3) - 128 8 | 9 | << _ :: binary-size(mp3_byte_size), id3_tag :: binary >> = mp3 10 | 11 | << "TAG", title :: binary-size(30), 12 | artist :: binary-size(30), 13 | album :: binary-size(30), 14 | year :: binary-size(4), 15 | _rest :: binary >> = id3_tag 16 | 17 | title = String.trim title, <<0>> 18 | artist = String.trim artist, <<0>> 19 | album = String.trim album, <<0>> 20 | 21 | IO.puts "#{artist} - #{title} (#{album}, #{year})" 22 | 23 | _ -> 24 | IO.puts "Couldn't open #{file_name}" 25 | 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /chapter_2/2_4/my_list.ex: -------------------------------------------------------------------------------- 1 | defmodule MyList do 2 | def flatten([]), do: [] 3 | 4 | def flatten([ head | tail ]) do 5 | flatten(head) ++ flatten(tail) 6 | end 7 | 8 | def flatten(head), do: [ head ] 9 | end 10 | 11 | -------------------------------------------------------------------------------- /chapter_3/metex/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | -------------------------------------------------------------------------------- /chapter_3/metex/README.md: -------------------------------------------------------------------------------- 1 | Metex 2 | ===== 3 | 4 | A simple Elixir app that reports the temperature given a location. 5 | 6 | ## Running Metex 7 | 8 | ```elixir 9 | iex> cities = ["Singapore", "Monaco", "Vatican City", "Hong Kong", "Macau"] 10 | 11 | iex> Metex.temperatures_of(cities) 12 | Hong Kong: 17.8°C, Macau: 18.4°C, Monaco: 8.8°C, Singapore: 28.6°C, Vatican City: 8.5°C 13 | ``` 14 | 15 | -------------------------------------------------------------------------------- /chapter_3/metex/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | config :metex, 6 | api_key: "969038ec2b87be02c5f1e3f1344ee286" 7 | 8 | # This configuration is loaded before any dependency and is restricted 9 | # to this project. If another project depends on this project, this 10 | # file won't be loaded nor affect the parent project. For this reason, 11 | # if you want to provide default values for your application for third- 12 | # party users, it should be done in your mix.exs file. 13 | 14 | # Sample configuration: 15 | # 16 | # config :logger, :console, 17 | # level: :info, 18 | # format: "$date $time [$level] $metadata$message\n", 19 | # metadata: [:user_id] 20 | 21 | # It is also possible to import configuration files, relative to this 22 | # directory. For example, you can emulate configuration per environment 23 | # by uncommenting the line below and defining dev.exs, test.exs and such. 24 | # Configuration from the imported file will override the ones defined 25 | # here (which is why it is important to import them last). 26 | # 27 | # import_config "#{Mix.env}.exs" 28 | -------------------------------------------------------------------------------- /chapter_3/metex/lib/coordinator.ex: -------------------------------------------------------------------------------- 1 | defmodule Metex.Coordinator do 2 | 3 | def loop(results \\ [], results_expected) do 4 | receive do 5 | {:ok, result} -> 6 | new_results = [result|results] 7 | if results_expected == Enum.count(new_results) do 8 | send self, :exit 9 | end 10 | loop(new_results, results_expected) 11 | :exit -> 12 | IO.puts(results |> Enum.sort |> Enum.join(", ")) 13 | _ -> 14 | loop(results, results_expected) 15 | end 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /chapter_3/metex/lib/metex.ex: -------------------------------------------------------------------------------- 1 | defmodule Metex do 2 | 3 | def temperatures_of(cities) do 4 | coordinator_pid = spawn(Metex.Coordinator, :loop, [[], Enum.count(cities)]) 5 | 6 | cities |> Enum.each(fn city -> 7 | worker_pid = spawn(Metex.Worker, :loop, []) 8 | send worker_pid, {coordinator_pid, city} 9 | end) 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /chapter_3/metex/lib/worker.ex: -------------------------------------------------------------------------------- 1 | defmodule Metex.Worker do 2 | 3 | def loop do 4 | receive do 5 | {sender_pid, location} -> 6 | send(sender_pid, {:ok, temperature_of(location)}) 7 | _ -> 8 | IO.puts "don't know how to process this message" 9 | end 10 | loop 11 | end 12 | 13 | defp temperature_of(location) do 14 | result = url_for(location) |> HTTPoison.get |> parse_response 15 | case result do 16 | {:ok, temp} -> 17 | "#{location}: #{temp}°C" 18 | :error -> 19 | "#{location} not found" 20 | end 21 | end 22 | 23 | defp url_for(location) do 24 | location = URI.encode(location) 25 | "http://api.openweathermap.org/data/2.5/weather?q=#{location}&appid=#{apikey}" 26 | end 27 | 28 | defp parse_response({:ok, %HTTPoison.Response{body: body, status_code: 200}}) do 29 | body |> JSON.decode! |> compute_temperature 30 | end 31 | 32 | defp parse_response(_) do 33 | :error 34 | end 35 | 36 | defp compute_temperature(json) do 37 | try do 38 | temp = (json["main"]["temp"] - 273.15) |> Float.round(1) 39 | {:ok, temp} 40 | rescue 41 | _ -> :error 42 | end 43 | end 44 | 45 | defp apikey do 46 | Application.fetch_env!(:metex, :api_key) 47 | end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /chapter_3/metex/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Metex.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :metex, 6 | version: "0.0.1", 7 | elixir: "~> 1.0", 8 | deps: deps] 9 | end 10 | 11 | def application do 12 | [applications: [:logger, :httpoison]] 13 | end 14 | 15 | defp deps do 16 | [ 17 | {:httpoison, "~> 0.9.0"}, 18 | {:json, "~> 0.3.0"} 19 | ] 20 | end 21 | 22 | end 23 | -------------------------------------------------------------------------------- /chapter_3/metex/mix.lock: -------------------------------------------------------------------------------- 1 | %{"certifi": {:hex, :certifi, "0.4.0", "a7966efb868b179023618d29a407548f70c52466bf1849b9e8ebd0e34b7ea11f", [:rebar3], []}, 2 | "hackney": {:hex, :hackney, "1.6.1", "ddd22d42db2b50e6a155439c8811b8f6df61a4395de10509714ad2751c6da817", [:rebar3], [{:ssl_verify_fun, "1.1.0", [hex: :ssl_verify_fun, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:certifi, "0.4.0", [hex: :certifi, optional: false]}]}, 3 | "httpoison": {:hex, :httpoison, "0.9.0", "68187a2daddfabbe7ca8f7d75ef227f89f0e1507f7eecb67e4536b3c516faddb", [:mix], [{:hackney, "~> 1.6.0", [hex: :hackney, optional: false]}]}, 4 | "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []}, 5 | "json": {:hex, :json, "0.3.2", "e40a15993024a0cb4f5e22816d6be34002cc4463c61789ed35ec94c41911ebee", [:mix], []}, 6 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, 7 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, 8 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.0", "edee20847c42e379bf91261db474ffbe373f8acb56e9079acb6038d4e0bf414f", [:rebar, :make], []}, 9 | "timex": {:hex, :timex, "0.13.3"}} 10 | -------------------------------------------------------------------------------- /chapter_3/metex/test/metex_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MetexTest do 2 | use ExUnit.Case 3 | 4 | test "the truth" do 5 | assert 1 + 1 == 2 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /chapter_3/metex/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /chapter_4/metex/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | -------------------------------------------------------------------------------- /chapter_4/metex/README.md: -------------------------------------------------------------------------------- 1 | Metex 2 | ===== 3 | 4 | `iex -S mix` 5 | 6 | 7 | ```elixir 8 | iex> Metex.Worker.start_link 9 | 10 | iex> Metex.Worker.get_temperature "Berlin" 11 | 12 | iex> Metex.Worker.get_stats 13 | 14 | iex> Metex.Worker.reset_stats 15 | 16 | iex> Metex.Worker.get_stats 17 | 18 | iex> Metex.Worker.stop 19 | ``` 20 | -------------------------------------------------------------------------------- /chapter_4/metex/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | config :metex, 6 | api_key: "969038ec2b87be02c5f1e3f1344ee286" 7 | 8 | # This configuration is loaded before any dependency and is restricted 9 | # to this project. If another project depends on this project, this 10 | # file won't be loaded nor affect the parent project. For this reason, 11 | # if you want to provide default values for your application for third- 12 | # party users, it should be done in your mix.exs file. 13 | 14 | # Sample configuration: 15 | # 16 | # config :logger, :console, 17 | # level: :info, 18 | # format: "$date $time [$level] $metadata$message\n", 19 | # metadata: [:user_id] 20 | 21 | # It is also possible to import configuration files, relative to this 22 | # directory. For example, you can emulate configuration per environment 23 | # by uncommenting the line below and defining dev.exs, test.exs and such. 24 | # Configuration from the imported file will override the ones defined 25 | # here (which is why it is important to import them last). 26 | # 27 | # import_config "#{Mix.env}.exs" 28 | -------------------------------------------------------------------------------- /chapter_4/metex/lib/metex.ex: -------------------------------------------------------------------------------- 1 | defmodule Metex do 2 | end 3 | -------------------------------------------------------------------------------- /chapter_4/metex/lib/worker.ex: -------------------------------------------------------------------------------- 1 | defmodule Metex.Worker do 2 | use GenServer 3 | 4 | @name MW 5 | 6 | ## Client API 7 | 8 | def start_link(opts \\ []) do 9 | GenServer.start_link(__MODULE__, :ok, opts ++ [name: MW]) 10 | end 11 | 12 | def get_temperature(location) do 13 | GenServer.call(@name, {:location, location}) 14 | end 15 | 16 | def get_stats do 17 | GenServer.call(@name, :get_stats) 18 | end 19 | 20 | def reset_stats do 21 | GenServer.cast(@name, :reset_stats) 22 | end 23 | 24 | def stop do 25 | GenServer.cast(@name, :stop) 26 | end 27 | 28 | ## Server Callbacks 29 | 30 | def init(:ok) do 31 | {:ok, %{}} 32 | end 33 | 34 | def handle_call({:location, location}, _from, stats) do 35 | case temperature_of(location) do 36 | {:ok, temp} -> 37 | new_stats = update_stats(stats, location) 38 | {:reply, "#{temp}°C", new_stats} 39 | 40 | _ -> 41 | 42 | {:reply, :error, stats} 43 | end 44 | end 45 | 46 | def handle_call(:get_stats, _from, stats) do 47 | {:reply, stats, stats} 48 | end 49 | 50 | def handle_cast(:reset_stats, _stats) do 51 | {:noreply, %{}} 52 | end 53 | 54 | def handle_cast(:stop, stats) do 55 | {:stop, :normal, stats} 56 | end 57 | 58 | def terminate(reason, stats) do 59 | # We could write to a file, database etc 60 | IO.puts "server terminated because of #{inspect reason}" 61 | inspect stats 62 | :ok 63 | end 64 | 65 | def handle_info(msg, stats) do 66 | IO.puts "received #{inspect msg}" 67 | {:noreply, stats} 68 | end 69 | 70 | ## Helper Functions 71 | 72 | defp temperature_of(location) do 73 | url_for(location) |> HTTPoison.get |> parse_response 74 | end 75 | 76 | defp url_for(location) do 77 | location = URI.encode(location) 78 | "http://api.openweathermap.org/data/2.5/weather?q=#{location}&appid=#{apikey}" 79 | end 80 | 81 | defp parse_response({:ok, %HTTPoison.Response{body: body, status_code: 200}}) do 82 | body |> JSON.decode! |> compute_temperature 83 | end 84 | 85 | defp parse_response(_) do 86 | :error 87 | end 88 | 89 | defp compute_temperature(json) do 90 | try do 91 | temp = (json["main"]["temp"] - 273.15) |> Float.round(1) 92 | {:ok, temp} 93 | rescue 94 | _ -> :error 95 | end 96 | end 97 | 98 | defp update_stats(old_stats, location) do 99 | Map.update(old_stats, location, 1, &(&1 + 1)) 100 | end 101 | 102 | defp apikey do 103 | Application.fetch_env!(:metex, :api_key) 104 | end 105 | 106 | end 107 | -------------------------------------------------------------------------------- /chapter_4/metex/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Metex.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :metex, 6 | version: "0.0.1", 7 | elixir: "~> 1.0", 8 | deps: deps] 9 | end 10 | 11 | def application do 12 | [applications: [:logger, :httpoison]] 13 | end 14 | 15 | defp deps do 16 | [ 17 | {:httpoison, "~> 0.9.0"}, 18 | {:json, "~> 0.3.0"} 19 | ] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /chapter_4/metex/mix.lock: -------------------------------------------------------------------------------- 1 | %{"certifi": {:hex, :certifi, "0.4.0", "a7966efb868b179023618d29a407548f70c52466bf1849b9e8ebd0e34b7ea11f", [:rebar3], []}, 2 | "hackney": {:hex, :hackney, "1.6.1", "ddd22d42db2b50e6a155439c8811b8f6df61a4395de10509714ad2751c6da817", [:rebar3], [{:ssl_verify_fun, "1.1.0", [hex: :ssl_verify_fun, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:certifi, "0.4.0", [hex: :certifi, optional: false]}]}, 3 | "httpoison": {:hex, :httpoison, "0.9.0", "68187a2daddfabbe7ca8f7d75ef227f89f0e1507f7eecb67e4536b3c516faddb", [:mix], [{:hackney, "~> 1.6.0", [hex: :hackney, optional: false]}]}, 4 | "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []}, 5 | "json": {:hex, :json, "0.3.2", "e40a15993024a0cb4f5e22816d6be34002cc4463c61789ed35ec94c41911ebee", [:mix], []}, 6 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, 7 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, 8 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.0", "edee20847c42e379bf91261db474ffbe373f8acb56e9079acb6038d4e0bf414f", [:rebar, :make], []}, 9 | "ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.4"}} 10 | -------------------------------------------------------------------------------- /chapter_4/metex/test/metex_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MetexTest do 2 | use ExUnit.Case 3 | 4 | test "the truth" do 5 | assert 1 + 1 == 2 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /chapter_4/metex/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /chapter_5/thy_supervisor/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | -------------------------------------------------------------------------------- /chapter_5/thy_supervisor/README.md: -------------------------------------------------------------------------------- 1 | Thy Supervisor 2 | ============= 3 | 4 | This is an exercise in implementing your own supervisor. 5 | 6 | -------------------------------------------------------------------------------- /chapter_5/thy_supervisor/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for third- 9 | # party users, it should be done in your mix.exs file. 10 | 11 | # Sample configuration: 12 | # 13 | # config :logger, :console, 14 | # level: :info, 15 | # format: "$date $time [$level] $metadata$message\n", 16 | # metadata: [:user_id] 17 | 18 | # It is also possible to import configuration files, relative to this 19 | # directory. For example, you can emulate configuration per environment 20 | # by uncommenting the line below and defining dev.exs, test.exs and such. 21 | # Configuration from the imported file will override the ones defined 22 | # here (which is why it is important to import them last). 23 | # 24 | # import_config "#{Mix.env}.exs" 25 | -------------------------------------------------------------------------------- /chapter_5/thy_supervisor/lib/thy_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule ThySupervisor do 2 | use GenServer 3 | 4 | ####### 5 | # API # 6 | ####### 7 | 8 | def start_link(child_spec_list) do 9 | GenServer.start_link(__MODULE__, [child_spec_list]) 10 | end 11 | 12 | def start_child(supervisor, child_spec) do 13 | GenServer.call(supervisor, {:start_child, child_spec}) 14 | end 15 | 16 | def terminate_child(supervisor, pid) when is_pid(pid) do 17 | GenServer.call(supervisor, {:terminate_child, pid}) 18 | end 19 | 20 | def restart_child(supervisor, pid, child_spec) when is_pid(pid) do 21 | GenServer.call(supervisor, {:restart_child, pid}) 22 | end 23 | 24 | def count_children(supervisor) do 25 | GenServer.call(supervisor, :count_children) 26 | end 27 | 28 | def which_children(supervisor) do 29 | GenServer.call(supervisor, :which_children) 30 | end 31 | 32 | ###################### 33 | # Callback Functions # 34 | ###################### 35 | 36 | def init([child_spec_list]) do 37 | Process.flag(:trap_exit, true) 38 | state = child_spec_list 39 | |> start_children 40 | |> Enum.into(%{}) 41 | 42 | {:ok, state} 43 | end 44 | 45 | def handle_call({:start_child, child_spec}, _from, state) do 46 | case start_child(child_spec) do 47 | {:ok, pid} -> 48 | new_state = state |> Map.put(pid, child_spec) 49 | {:reply, {:ok, pid}, new_state} 50 | :error -> 51 | {:reply, {:error, "error starting child"}, state} 52 | end 53 | end 54 | 55 | def handle_call({:terminate_child, pid}, _from, state) do 56 | case terminate_child(pid) do 57 | :ok -> 58 | new_state = state |> Map.delete(pid) 59 | {:reply, :ok, new_state} 60 | :error -> 61 | {:reply, {:error, "error terminating child"}, state} 62 | end 63 | end 64 | 65 | def handle_call({:restart_child, old_pid}, _from, state) do 66 | case Map.fetch(state, old_pid) do 67 | {:ok, child_spec} -> 68 | case restart_child(old_pid, child_spec) do 69 | {:ok, {pid, child_spec}} -> 70 | new_state = state 71 | |> Map.delete(old_pid) 72 | |> Map.put(pid, child_spec) 73 | {:reply, {:ok, pid}, new_state} 74 | :error -> 75 | {:reply, {:error, "error restarting child"}, state} 76 | end 77 | _ -> 78 | {:reply, :ok, state} 79 | end 80 | end 81 | 82 | def handle_call(:count_children, _from, state) do 83 | {:reply, Enum.count(state), state} 84 | end 85 | 86 | def handle_call(:which_children, _from, state) do 87 | {:reply, state, state} 88 | end 89 | 90 | def handle_info({:EXIT, from, :normal}, state) do 91 | new_state = state |> Map.delete(from) 92 | {:noreply, new_state} 93 | end 94 | 95 | def handle_info({:EXIT, from, :killed}, state) do 96 | new_state = state |> Map.delete(from) 97 | {:noreply, new_state} 98 | end 99 | 100 | def handle_info({:EXIT, old_pid, _reason}, state) do 101 | case Map.fetch(state, old_pid) do 102 | {:ok, child_spec} -> 103 | case restart_child(old_pid, child_spec) do 104 | {:ok, {pid, child_spec}} -> 105 | new_state = state 106 | |> Map.delete(old_pid) 107 | |> Map.put(pid, child_spec) 108 | {:noreply, new_state} 109 | :error -> 110 | {:noreply, state} 111 | end 112 | _ -> 113 | {:noreply, state} 114 | end 115 | end 116 | 117 | def terminate(_reason, state) do 118 | terminate_children(state) 119 | :ok 120 | end 121 | 122 | ##################### 123 | # Private Functions # 124 | ##################### 125 | 126 | defp start_children([child_spec|rest]) do 127 | case start_child(child_spec) do 128 | {:ok, pid} -> 129 | [{pid, child_spec}|start_children(rest)] 130 | :error -> 131 | :error 132 | end 133 | end 134 | 135 | defp start_children([]), do: [] 136 | 137 | defp start_child({mod, fun, args}) do 138 | case apply(mod, fun, args) do 139 | pid when is_pid(pid) -> 140 | Process.link(pid) 141 | {:ok, pid} 142 | _ -> 143 | :error 144 | end 145 | end 146 | 147 | defp terminate_children([]) do 148 | :ok 149 | end 150 | 151 | defp terminate_children(child_specs) do 152 | child_specs |> Enum.each(fn {pid, _} -> terminate_child(pid) end) 153 | end 154 | 155 | defp terminate_child(pid) do 156 | Process.exit(pid, :kill) 157 | :ok 158 | end 159 | 160 | defp restart_child(pid, child_spec) when is_pid(pid) do 161 | case terminate_child(pid) do 162 | :ok -> 163 | case start_child(child_spec) do 164 | {:ok, new_pid} -> 165 | {:ok, {new_pid, child_spec}} 166 | :error -> 167 | :error 168 | end 169 | :error -> 170 | :error 171 | end 172 | end 173 | 174 | end 175 | -------------------------------------------------------------------------------- /chapter_5/thy_supervisor/lib/thy_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule ThyWorker do 2 | def start_link do 3 | spawn(fn -> loop end) 4 | end 5 | 6 | def loop do 7 | receive do 8 | :stop -> :ok 9 | 10 | msg -> 11 | IO.inspect msg 12 | loop 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /chapter_5/thy_supervisor/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ThySupervisor.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :thy_supervisor, 6 | version: "0.0.1", 7 | elixir: "~> 1.0", 8 | deps: deps] 9 | end 10 | 11 | def application do 12 | [applications: [:logger]] 13 | end 14 | 15 | defp deps do 16 | [] 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /chapter_5/thy_supervisor/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /chapter_5/thy_supervisor/test/thy_supervisor_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ThySupervisorTest do 2 | use ExUnit.Case 3 | 4 | test "can be initialized given child specs" do 5 | assert {:ok, _} = ThySupervisor.start_link(child_spec_list) 6 | end 7 | 8 | test "a child can be started" do 9 | {:ok, sup_pid} = ThySupervisor.start_link(child_spec_list) 10 | {:ok, _child_pid} = ThySupervisor.start_child(sup_pid, {ThyWorker, :start_link, []}) 11 | assert 4 == ThySupervisor.count_children(sup_pid) 12 | end 13 | 14 | test "a child can be terminated" do 15 | {:ok, sup_pid} = ThySupervisor.start_link([]) 16 | {:ok, child_pid} = ThySupervisor.start_child(sup_pid, {ThyWorker, :start_link, []}) 17 | 18 | assert :ok == ThySupervisor.terminate_child(sup_pid, child_pid) 19 | assert Process.alive?(sup_pid) 20 | assert 0 == ThySupervisor.count_children(sup_pid) 21 | end 22 | 23 | test "restarts an abnormally terminated child" do 24 | {:ok, sup_pid} = ThySupervisor.start_link([]) 25 | {:ok, child_pid} = ThySupervisor.start_child(sup_pid, {ThyWorker, :start_link, []}) 26 | 27 | Process.exit(child_pid, :crash) 28 | refute Process.alive?(child_pid) 29 | 30 | new_child_pid = ThySupervisor.which_children(sup_pid) |> Map.keys |> List.first 31 | 32 | assert 1 == ThySupervisor.count_children(sup_pid) 33 | assert is_pid(new_child_pid) 34 | assert new_child_pid != child_pid 35 | end 36 | 37 | defp child_spec_list do 38 | [ 39 | {ThyWorker, :start_link, []}, 40 | {ThyWorker, :start_link, []}, 41 | {ThyWorker, :start_link, []}, 42 | ] 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-1/README.md: -------------------------------------------------------------------------------- 1 | Pooly 2 | ===== 3 | 4 | ![](http://i.imgur.com/NobRZq0.png) 5 | 6 | > Definition of POOLY 7 | > 8 | > having many pools 9 | > 10 | > – Merriam-Webster Dictionary 11 | 12 | _Pooly_ is a worker pool library inspired by other Erlang worker pool libraries such as [poolboy](https://github.com/devinus/poolboy), [pooler](https://github.com/seth/pooler) and [ppool](http://learnyousomeerlang.com/building-applications-with-otp) (from [Chapter 18](http://learnyousomeerlang.com/building-applications-with-otp) of _Learn You Some Erlang For Great Good_). 13 | 14 | The whole point of this exercise is to make this project a part of the example project in Chapter 6 of the [book](http://www.exotpbook.com). 15 | 16 | ## API 17 | 18 | ```elixir 19 | alias Pooly, as: P 20 | 21 | P.start_link 22 | P.stop 23 | ``` 24 | 25 | ### Starting a pool 26 | 27 | This creates a named pool, attach it to the top-level supervisor and starts a bunch of workers. 28 | 29 | ### Worker Arguments 30 | 31 | ```elixir 32 | pools_config = 33 | [ 34 | [name: "Pool1", 35 | mfa: {SampleWorker, :start_link, []}, 36 | size: 2, 37 | max_overflow: 10 38 | ], 39 | [name: "Pool2", 40 | mfa: {SampleWorker, :start_link, []}, 41 | size: 2, 42 | max_overflow: 0 43 | ], 44 | ] 45 | 46 | 47 | P.start_pool(pool_config) 48 | ``` 49 | 50 | ### Stopping a pool 51 | 52 | ```elixir 53 | P.stop_pool("Pool1") 54 | ``` 55 | 56 | ### Getting workers 57 | 58 | This is the most basic version: 59 | 60 | ```elixir 61 | pid = P.checkout("Pool1") 62 | ``` 63 | 64 | `checkout/3` takes two other arguments. `block` (boolean) and `timeout`. The defaults are `true` and `5000` respectively. If `block` is true, the consumer process will wait for `timeout` milliseconds before timing out. Note that you can pass in `:infinity` as a timeout value. 65 | 66 | For example, this consumer process will wait indefinitely for a process to be available: 67 | 68 | ```elixir 69 | worker_pid = P.checkout("Pool1", true, :infinity) 70 | ``` 71 | 72 | ### Return workers back to the pool 73 | 74 | Returning workers is straightforward: 75 | 76 | ```elixir 77 | P.checkin("Pool1", worker_pid) do 78 | ``` 79 | 80 | ### Getting the status of a pool 81 | 82 | ```elixir 83 | P.status("Pool1") 84 | ``` 85 | 86 | # Version 1 87 | 88 | Although the first version is the most basic, it will get us a pretty long way, since it sets up the framework of the versions to come. 89 | 90 | ## Characteristics 91 | 92 | * Supports a _single_ pool 93 | * Supports a _fixed_ number of workers 94 | * No queuing 95 | * No recovery when consumer and/or worker process fail 96 | 97 | ## Details 98 | 99 | When Pooly first starts (`Pooly.start_link`) it is just this: 100 | 101 | ``` 102 | [Pooly.Supervisor] 103 | / 104 | / 105 | [Pooly.Server] 106 | ``` 107 | 108 | This is because there is no pool to be started yet. To do that: 109 | 110 | ```elixir 111 | Pool.start_pool(:some_worker_pool, {SomeWorker, :start_link, []}) 112 | ``` 113 | 114 | This creates a `Pooly.WorkerSupervisor` with a `:simple_one_for_one` strategy that essentially makes it a `SomeWorker` process factory. This is the _final_ state of the supervision tree, with no workers started yet. 115 | 116 | 117 | ``` 118 | [Pooly.Supervisor] 119 | / \ 120 | / \ 121 | [Pooly.Server] [Pooly.WorkerSupervisor] 122 | ``` 123 | 124 | Next, we need to work on limiting the number of workers. In order for this to happen, we need to introduce another variable. One option we _could_ do is: 125 | 126 | ```elixir 127 | Pool.start_pool(:some_worker_pool, [5, {SomeWorker, :start_link, []]}) 128 | ``` 129 | 130 | However, someone else taking a look at the code (that will be _you_, 2 weeks later) will be left wondering what exactly `5` does. A better way of expressing it would be using a _keyword list_: 131 | 132 | ```elixir 133 | [ 134 | mfa: {SomeWorker, :start_link, []}, 135 | size: 5 136 | ] 137 | ``` 138 | 139 | With a size, we can create a fixed number of workers each time the supervisor starts. Put another way, the supervisor will create `size` number of workers. 140 | 141 | ### Checking out and in workers 142 | 143 | The notion of checking in and out of workers is just like acquiring and releasing of a resource. In this case, the resource is the an available worker, represented by a process id. 144 | 145 | To support this operation, we have the following functions: 146 | 147 | ```elixir 148 | Pooly.checkout # returns an available worker pid, or :noproc if unavailable 149 | ``` 150 | 151 | When done, it the consumer of the worker pid (the process that did the previously check-out) must check the worker pid back in, otherwise, it will cause a resource starvation. An example could be a single process checking out every single worker, and not checking in back to the pool. We will fix this limitation later on. To check-in a worker: 152 | 153 | ```elixir 154 | Pooly.checkin(:some_worker_pool, worker_pid) 155 | ``` 156 | 157 | 158 | ### Server state 159 | 160 | At the end of this iteration, the server state should look like: 161 | 162 | ```elixir 163 | defmodule State do 164 | defstruct supervisor: nil, 165 | workers: [], 166 | monitors: :ets.new(:monitors, [:private]), 167 | size: 5 168 | end 169 | ``` 170 | 171 | ### Running it 172 | 173 | __TODO:__ _Create a sample worker and put `Pooly` through its paces_ 174 | 175 | # Version 2 176 | 177 | ## Characteristics 178 | 179 | * Supports a _single_ pool 180 | * Supports a _fixed_ number of workers 181 | * No queuing 182 | * recovery when consumer and/or worker process fail 183 | 184 | ### Linking 185 | 186 | Besides checking in a worker, the worker could crash too. Othertimes, the worker could exit normally. Since the supervisor stance on restarting crashed workers is `:temporary`, this means that workers are never restarted. That's because in general we never know whether a worker should be restarted. While you can build this into the implementation like having it as a setting, we will keep it simple. 187 | 188 | In order to handle these various situations, we need to know when something happens to a checked out worker process. Our worker processes should crash too if the server crashes. Links (and trapping exits) are perfect for this. What should happen when a worker crashes? Well, the pool should automatically create a new worker, no questions asked. 189 | 190 | ### Monitoring 191 | 192 | How do we know when a consumer process dies? Monitors! What should happen then when we detect that a consumer process dies? How can we retrieve the worker? (Monitor reference!) 193 | 194 | # Version 3 195 | 196 | ## Characteristics 197 | 198 | * Supports _multiple_ pools by dynamically creating supervisors 199 | * Supports a _variable_ number of workers 200 | 201 | So far, our worker pool can only handle one pool, which isn't terribly useful. In this iteration, we will add support for multiple pools and finally add more bells and whistles (be specific about this). 202 | 203 | ### Supporting multiple queues 204 | 205 | The most straight forward way would be to design the supervision tree like so: 206 | 207 | ``` 208 | [Pooly.Supervisor] 209 | / / | \ 210 | / / | \ 211 | [Pooly.Server] | \ 212 | / | \ 213 | / | [Pooly.WorkerSupervisor] 214 | [Pooly.WorkerSupervisor] 215 | | 216 | [Pooly.WorkerSupervisor] 217 | ``` 218 | 219 | ### Error Kernels and Error Isolation 220 | 221 | We are essentially sticking more `WorkerSupervisor`'s into `Pooly.Supervisor`. This is a bad design. The issue here is the _error kernel_. Issues with any of the `WorkerSupervisor`s shouldn't affect the `Pooly.Server`. (More reasons needed, separation of concerns). It pays to think about what happens when a process crashes and who gets affected. 222 | 223 | 224 | The fix is to add another supervisor to handle all the worker supervisors, say a `Pooly.WorkersSupervisor`. This _might_ design we are shooting for: 225 | 226 | ``` 227 | [Pooly.Supervisor] 228 | / \ 229 | / \ 230 | [Pooly.Server] [Pooly.WorkersSupervisor] 231 | / | \ 232 | / | [Pooly.WorkerSupervisor] 233 | [Pooly.WorkerSupervisor] | 234 | [Pooly.WorkerSupervisor] 235 | ``` 236 | 237 | Do you notice another problem? 238 | 239 | Currently, the poor `Pooly.Server` process has to handle every request that is meant for _any_ pool. 240 | 241 | This means that the lone server process might pose a bottle neck if messages to it come fast and furious, and could potentially flood it's mailbox. `Pooly.Server` also presents a single point of failure, since it contains the state of _every_ pool, and having the server process dead renders the pools useless. 242 | 243 | The simplest thing to do is to have a dedicated `Pool.Server` process for each pool. So let's stick `Pooly.Server` into `Pool.WorkersSupervisor` ok? Not ok! If we did that, then `WorkersSupervisor` is going to have to supervise `Pool.Server`s too. 244 | 245 | > A good exercise would be to quickly sketch out how you think the supervision tree should look like. 246 | 247 | ``` 248 | [Pooly.Supervisor] 249 | \ 250 | \ 251 | [Pooly.PoolsSupervisor] 252 | / | \ 253 | / | [*] 254 | [Pooly.PoolSupervisor] | 255 | / \ [*] 256 | [Pooly.Server] [Pooly.WorkerSupervisor] 257 | ``` 258 | 259 | You might find it slightly weird (I do!) that the `Pooly.Supervisor` is only supervising one child. Why couldn't we let it supervise the `Pooly.PoolSupervisor`s instead? Well, we need something to take the place of `Pool.Server`. In particular, we need a process to start the pools! Now, starting the pool involves: 260 | 261 | * Telling `PoolsSupervisor` to start a `Pooly.PoolSupervisor` child 262 | * `Pooly.PoolSupervisor` will initialize a `Pooly.Server` and a `Pooly.WorkerSupervisor`. 263 | 264 | In order words, this is how we want the design to look like: 265 | 266 | ``` 267 | [Pooly.Supervisor] 268 | / \ 269 | / \ 270 | [Pooly.PoolStarter] [Pooly.PoolsSupervisor] 271 | / | \ 272 | / | [*] 273 | [Pooly.PoolSupervisor] | 274 | / \ [*] 275 | [Pooly.Server] [Pooly.WorkerSupervisor] 276 | ``` 277 | 278 | The `Pooly.PoolStarter` process is a simple GenServer that is stateless, since there is not need for it to keep any. 279 | 280 | 281 | ### Server state 282 | 283 | ```elixir 284 | defmodule State do 285 | defstruct pool_sup: nil, 286 | worker_sup: nil, 287 | monitors: nil, 288 | monitors: nil, 289 | size: nil, 290 | workers: nil, 291 | name: nil, 292 | mfa: nil, 293 | end 294 | ``` 295 | 296 | # Version 4 297 | 298 | ## Features 299 | 300 | * Transactions 301 | * Overflowing of workers 302 | * Waiting and Queuing 303 | 304 | ### Implementing automatic checkout/checkin with transactions 305 | 306 | Up until now, it is the onus of the consumer process to check back in a worker process once it is done with it. However, this is an unreasonable requirement, and we can do better. Just like a database transaction, once the consumer process is done with it, we can automatically have the worker process checked back in. How do we do that? 307 | 308 | ### Blocking and Queuing 309 | 310 | When all workers are busy, a consumer that is willing to wait will be queued up. In this implementation, that is the default behaviour. It is relatively straightforward to implement a non-blocking consumer. (Just have a parameter which says `block`, and if it says `false`, return something like `{:error, full}`) 311 | 312 | For a consumer that blocks, once a worker is checked back into the pool, the consumer is then unblocked and given the worker. 313 | 314 | ### Supporting a variable number of workers 315 | 316 | Next, we want to add some flexibility to `Pooly`. In particular, we want to specify a _maximum overflow_ of workers. What does this buy us? Consider the following scenario. (More research. See Sasa's [article](www.theerlangelist.com/2013/04/parallelizing-independent-tasks.html) on setting size to zero and overflow to 5, essentially for _dynamic_ workers) 317 | 318 | ### Server state 319 | 320 | ```elixir 321 | defmodule State do 322 | defstruct pool_sup: nil, 323 | worker_sup: nil, 324 | monitors: nil, 325 | monitors: nil, 326 | size: nil, 327 | workers: nil, 328 | name: nil, 329 | mfa: nil, 330 | waiting: nil, 331 | overflow: nil, 332 | max_overflow: nil 333 | end 334 | ``` 335 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-1/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for third- 9 | # party users, it should be done in your mix.exs file. 10 | 11 | # Sample configuration: 12 | # 13 | # config :logger, :console, 14 | # level: :info, 15 | # format: "$date $time [$level] $metadata$message\n", 16 | # metadata: [:user_id] 17 | 18 | # It is also possible to import configuration files, relative to this 19 | # directory. For example, you can emulate configuration per environment 20 | # by uncommenting the line below and defining dev.exs, test.exs and such. 21 | # Configuration from the imported file will override the ones defined 22 | # here (which is why it is important to import them last). 23 | # 24 | # import_config "#{Mix.env}.exs" 25 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-1/lib/pooly.ex: -------------------------------------------------------------------------------- 1 | defmodule Pooly do 2 | use Application 3 | 4 | def start(_type, _args) do 5 | pool_config = [mfa: {SampleWorker, :start_link, []}, size: 5] 6 | start_pool(pool_config) 7 | end 8 | 9 | def start_pool(pool_config) do 10 | Pooly.Supervisor.start_link(pool_config) 11 | end 12 | 13 | def checkout do 14 | Pooly.Server.checkout 15 | end 16 | 17 | def checkin(worker_pid) do 18 | Pooly.Server.checkin(worker_pid) 19 | end 20 | 21 | def status do 22 | Pooly.Server.status 23 | end 24 | 25 | end 26 | 27 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-1/lib/pooly/sample_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule SampleWorker do 2 | use GenServer 3 | 4 | def start_link(_) do 5 | GenServer.start_link(__MODULE__, :ok, []) 6 | end 7 | 8 | def stop(pid) do 9 | GenServer.call(pid, :stop) 10 | end 11 | 12 | def handle_call(:stop, _from, state) do 13 | {:stop, :normal, :ok, state} 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-1/lib/pooly/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Pooly.Server do 2 | use GenServer 3 | import Supervisor.Spec 4 | 5 | defmodule State do 6 | defstruct sup: nil, worker_sup: nil, monitors: nil, size: nil, workers: nil, mfa: nil 7 | end 8 | 9 | def start_link(sup, pool_config) do 10 | GenServer.start_link(__MODULE__, [sup, pool_config], name: __MODULE__) 11 | end 12 | 13 | def checkout do 14 | GenServer.call(__MODULE__, :checkout) 15 | end 16 | 17 | def checkin(worker_pid) do 18 | GenServer.cast(__MODULE__, {:checkin, worker_pid}) 19 | end 20 | 21 | def status do 22 | GenServer.call(__MODULE__, :status) 23 | end 24 | 25 | ############# 26 | # Callbacks # 27 | ############# 28 | 29 | def init([sup, pool_config]) when is_pid(sup) do 30 | monitors = :ets.new(:monitors, [:private]) 31 | init(pool_config, %State{sup: sup, monitors: monitors}) 32 | end 33 | 34 | def init([{:mfa, mfa}|rest], state) do 35 | init(rest, %{state | mfa: mfa}) 36 | end 37 | 38 | def init([{:size, size}|rest], state) do 39 | init(rest, %{state | size: size}) 40 | end 41 | 42 | def init([_|rest], state) do 43 | init(rest, state) 44 | end 45 | 46 | def init([], state) do 47 | send(self(), :start_worker_supervisor) 48 | {:ok, state} 49 | end 50 | 51 | def handle_call(:checkout, {from_pid, _ref}, %{workers: workers, monitors: monitors} = state) do 52 | case workers do 53 | [worker|rest] -> 54 | ref = Process.monitor(from_pid) 55 | true = :ets.insert(monitors, {worker, ref}) 56 | {:reply, worker, %{state | workers: rest}} 57 | 58 | [] -> 59 | {:reply, :noproc, state} 60 | end 61 | end 62 | 63 | def handle_call(:status, _from, %{workers: workers, monitors: monitors} = state) do 64 | {:reply, {length(workers), :ets.info(monitors, :size)}, state} 65 | end 66 | 67 | 68 | def handle_cast({:checkin, worker}, %{workers: workers, monitors: monitors} = state) do 69 | case :ets.lookup(monitors, worker) do 70 | [{pid, ref}] -> 71 | true = Process.demonitor(ref) 72 | true = :ets.delete(monitors, pid) 73 | {:noreply, %{state | workers: [pid|workers]}} 74 | [] -> 75 | {:noreply, state} 76 | end 77 | end 78 | 79 | def handle_info(:start_worker_supervisor, state = %{sup: sup, mfa: mfa, size: size}) do 80 | {:ok, worker_sup} = Supervisor.start_child(sup, supervisor_spec(mfa)) 81 | workers = prepopulate(size, worker_sup) 82 | {:noreply, %{state | worker_sup: worker_sup, workers: workers}} 83 | end 84 | 85 | ##################### 86 | # Private Functions # 87 | ##################### 88 | 89 | defp prepopulate(size, sup) do 90 | prepopulate(size, sup, []) 91 | end 92 | 93 | defp prepopulate(size, _sup, workers) when size < 1 do 94 | workers 95 | end 96 | 97 | defp prepopulate(size, sup, workers) do 98 | prepopulate(size-1, sup, [new_worker(sup) | workers]) 99 | end 100 | 101 | defp new_worker(sup) do 102 | {:ok, worker} = Supervisor.start_child(sup, [[]]) 103 | worker 104 | end 105 | 106 | defp supervisor_spec(mfa) do 107 | opts = [restart: :temporary] 108 | supervisor(Pooly.WorkerSupervisor, [mfa], opts) 109 | end 110 | 111 | end 112 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-1/lib/pooly/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Pooly.Supervisor do 2 | use Supervisor 3 | 4 | def start_link(pool_config) do 5 | Supervisor.start_link(__MODULE__, pool_config) 6 | end 7 | 8 | def init(pool_config) do 9 | children = [ 10 | worker(Pooly.Server, [self(), pool_config]) 11 | ] 12 | 13 | opts = [strategy: :one_for_all] 14 | 15 | supervise(children, opts) 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-1/lib/pooly/worker_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Pooly.WorkerSupervisor do 2 | use Supervisor 3 | 4 | def start_link({_,_,_} = mfa) do 5 | Supervisor.start_link(__MODULE__, mfa) 6 | end 7 | 8 | def init({m,f,a}) do 9 | worker_opts = [restart: :permanent, 10 | shutdown: 5000, 11 | function: f] 12 | 13 | children = [worker(m, a, worker_opts)] 14 | opts = [strategy: :simple_one_for_one, 15 | max_restarts: 5, 16 | max_seconds: 5] 17 | 18 | supervise(children, opts) 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-1/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Pooly.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :pooly, 6 | version: "0.1.0", 7 | elixir: "~> 1.0", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps()] 11 | end 12 | 13 | def application do 14 | [applications: [:logger], 15 | mod: {Pooly, []}] # hardcoded first. 16 | end 17 | 18 | defp deps do 19 | [] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-1/test/pooly_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PoolyTest do 2 | use ExUnit.Case 3 | 4 | test "the truth" do 5 | assert 1 + 1 == 2 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-1/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-2/README.md: -------------------------------------------------------------------------------- 1 | Pooly 2 | ===== 3 | 4 | ![](http://i.imgur.com/NobRZq0.png) 5 | 6 | > Definition of POOLY 7 | > 8 | > having many pools 9 | > 10 | > – Merriam-Webster Dictionary 11 | 12 | _Pooly_ is a worker pool library inspired by other Erlang worker pool libraries such as [poolboy](https://github.com/devinus/poolboy), [pooler](https://github.com/seth/pooler) and [ppool](http://learnyousomeerlang.com/building-applications-with-otp) (from [Chapter 18](http://learnyousomeerlang.com/building-applications-with-otp) of _Learn You Some Erlang For Great Good_). 13 | 14 | The whole point of this exercise is to make this project a part of the example project in Chapter 6 of the [book](http://www.exotpbook.com). 15 | 16 | ## API 17 | 18 | ```elixir 19 | alias Pooly, as: P 20 | 21 | P.start_link 22 | P.stop 23 | ``` 24 | 25 | ### Starting a pool 26 | 27 | This creates a named pool, attach it to the top-level supervisor and starts a bunch of workers. 28 | 29 | ### Worker Arguments 30 | 31 | ```elixir 32 | pools_config = 33 | [ 34 | [name: "Pool1", 35 | mfa: {SampleWorker, :start_link, []}, 36 | size: 2, 37 | max_overflow: 10 38 | ], 39 | [name: "Pool2", 40 | mfa: {SampleWorker, :start_link, []}, 41 | size: 2, 42 | max_overflow: 0 43 | ], 44 | ] 45 | 46 | 47 | P.start_pool(pool_config) 48 | ``` 49 | 50 | ### Stopping a pool 51 | 52 | ```elixir 53 | P.stop_pool("Pool1") 54 | ``` 55 | 56 | ### Getting workers 57 | 58 | This is the most basic version: 59 | 60 | ```elixir 61 | pid = P.checkout("Pool1") 62 | ``` 63 | 64 | `checkout/3` takes two other arguments. `block` (boolean) and `timeout`. The defaults are `true` and `5000` respectively. If `block` is true, the consumer process will wait for `timeout` milliseconds before timing out. Note that you can pass in `:infinity` as a timeout value. 65 | 66 | For example, this consumer process will wait indefinitely for a process to be available: 67 | 68 | ```elixir 69 | worker_pid = P.checkout("Pool1", true, :infinity) 70 | ``` 71 | 72 | ### Return workers back to the pool 73 | 74 | Returning workers is straightforward: 75 | 76 | ```elixir 77 | P.checkin("Pool1", worker_pid) do 78 | ``` 79 | 80 | ### Getting the status of a pool 81 | 82 | ```elixir 83 | P.status("Pool1") 84 | ``` 85 | 86 | # Version 1 87 | 88 | Although the first version is the most basic, it will get us a pretty long way, since it sets up the framework of the versions to come. 89 | 90 | ## Characteristics 91 | 92 | * Supports a _single_ pool 93 | * Supports a _fixed_ number of workers 94 | * No queuing 95 | * No recovery when consumer and/or worker process fail 96 | 97 | ## Details 98 | 99 | When Pooly first starts (`Pooly.start_link`) it is just this: 100 | 101 | ``` 102 | [Pooly.Supervisor] 103 | / 104 | / 105 | [Pooly.Server] 106 | ``` 107 | 108 | This is because there is no pool to be started yet. To do that: 109 | 110 | ```elixir 111 | Pool.start_pool(:some_worker_pool, {SomeWorker, :start_link, []}) 112 | ``` 113 | 114 | This creates a `Pooly.WorkerSupervisor` with a `:simple_one_for_one` strategy that essentially makes it a `SomeWorker` process factory. This is the _final_ state of the supervision tree, with no workers started yet. 115 | 116 | 117 | ``` 118 | [Pooly.Supervisor] 119 | / \ 120 | / \ 121 | [Pooly.Server] [Pooly.WorkerSupervisor] 122 | ``` 123 | 124 | Next, we need to work on limiting the number of workers. In order for this to happen, we need to introduce another variable. One option we _could_ do is: 125 | 126 | ```elixir 127 | Pool.start_pool(:some_worker_pool, [5, {SomeWorker, :start_link, []]}) 128 | ``` 129 | 130 | However, someone else taking a look at the code (that will be _you_, 2 weeks later) will be left wondering what exactly `5` does. A better way of expressing it would be using a _keyword list_: 131 | 132 | ```elixir 133 | [ 134 | mfa: {SomeWorker, :start_link, []}, 135 | size: 5 136 | ] 137 | ``` 138 | 139 | With a size, we can create a fixed number of workers each time the supervisor starts. Put another way, the supervisor will create `size` number of workers. 140 | 141 | ### Checking out and in workers 142 | 143 | The notion of checking in and out of workers is just like acquiring and releasing of a resource. In this case, the resource is the an available worker, represented by a process id. 144 | 145 | To support this operation, we have the following functions: 146 | 147 | ```elixir 148 | Pooly.checkout # returns an available worker pid, or :noproc if unavailable 149 | ``` 150 | 151 | When done, it the consumer of the worker pid (the process that did the previously check-out) must check the worker pid back in, otherwise, it will cause a resource starvation. An example could be a single process checking out every single worker, and not checking in back to the pool. We will fix this limitation later on. To check-in a worker: 152 | 153 | ```elixir 154 | Pooly.checkin(:some_worker_pool, worker_pid) 155 | ``` 156 | 157 | 158 | ### Server state 159 | 160 | At the end of this iteration, the server state should look like: 161 | 162 | ```elixir 163 | defmodule State do 164 | defstruct supervisor: nil, 165 | workers: [], 166 | monitors: :ets.new(:monitors, [:private]), 167 | size: 5 168 | end 169 | ``` 170 | 171 | ### Running it 172 | 173 | __TODO:__ _Create a sample worker and put `Pooly` through its paces_ 174 | 175 | # Version 2 176 | 177 | ## Characteristics 178 | 179 | * Supports a _single_ pool 180 | * Supports a _fixed_ number of workers 181 | * No queuing 182 | * recovery when consumer and/or worker process fail 183 | 184 | ### Linking 185 | 186 | Besides checking in a worker, the worker could crash too. Othertimes, the worker could exit normally. Since the supervisor stance on restarting crashed workers is `:temporary`, this means that workers are never restarted. That's because in general we never know whether a worker should be restarted. While you can build this into the implementation like having it as a setting, we will keep it simple. 187 | 188 | In order to handle these various situations, we need to know when something happens to a checked out worker process. Our worker processes should crash too if the server crashes. Links (and trapping exits) are perfect for this. What should happen when a worker crashes? Well, the pool should automatically create a new worker, no questions asked. 189 | 190 | ### Monitoring 191 | 192 | How do we know when a consumer process dies? Monitors! What should happen then when we detect that a consumer process dies? How can we retrieve the worker? (Monitor reference!) 193 | 194 | # Version 3 195 | 196 | ## Characteristics 197 | 198 | * Supports _multiple_ pools by dynamically creating supervisors 199 | * Supports a _variable_ number of workers 200 | 201 | So far, our worker pool can only handle one pool, which isn't terribly useful. In this iteration, we will add support for multiple pools and finally add more bells and whistles (be specific about this). 202 | 203 | ### Supporting multiple queues 204 | 205 | The most straight forward way would be to design the supervision tree like so: 206 | 207 | ``` 208 | [Pooly.Supervisor] 209 | / / | \ 210 | / / | \ 211 | [Pooly.Server] | \ 212 | / | \ 213 | / | [Pooly.WorkerSupervisor] 214 | [Pooly.WorkerSupervisor] 215 | | 216 | [Pooly.WorkerSupervisor] 217 | ``` 218 | 219 | ### Error Kernels and Error Isolation 220 | 221 | We are essentially sticking more `WorkerSupervisor`'s into `Pooly.Supervisor`. This is a bad design. The issue here is the _error kernel_. Issues with any of the `WorkerSupervisor`s shouldn't affect the `Pooly.Server`. (More reasons needed, separation of concerns). It pays to think about what happens when a process crashes and who gets affected. 222 | 223 | 224 | The fix is to add another supervisor to handle all the worker supervisors, say a `Pooly.WorkersSupervisor`. This _might_ design we are shooting for: 225 | 226 | ``` 227 | [Pooly.Supervisor] 228 | / \ 229 | / \ 230 | [Pooly.Server] [Pooly.WorkersSupervisor] 231 | / | \ 232 | / | [Pooly.WorkerSupervisor] 233 | [Pooly.WorkerSupervisor] | 234 | [Pooly.WorkerSupervisor] 235 | ``` 236 | 237 | Do you notice another problem? 238 | 239 | Currently, the poor `Pooly.Server` process has to handle every request that is meant for _any_ pool. 240 | 241 | This means that the lone server process might pose a bottle neck if messages to it come fast and furious, and could potentially flood it's mailbox. `Pooly.Server` also presents a single point of failure, since it contains the state of _every_ pool, and having the server process dead renders the pools useless. 242 | 243 | The simplest thing to do is to have a dedicated `Pool.Server` process for each pool. So let's stick `Pooly.Server` into `Pool.WorkersSupervisor` ok? Not ok! If we did that, then `WorkersSupervisor` is going to have to supervise `Pool.Server`s too. 244 | 245 | > A good exercise would be to quickly sketch out how you think the supervision tree should look like. 246 | 247 | ``` 248 | [Pooly.Supervisor] 249 | \ 250 | \ 251 | [Pooly.PoolsSupervisor] 252 | / | \ 253 | / | [*] 254 | [Pooly.PoolSupervisor] | 255 | / \ [*] 256 | [Pooly.Server] [Pooly.WorkerSupervisor] 257 | ``` 258 | 259 | You might find it slightly weird (I do!) that the `Pooly.Supervisor` is only supervising one child. Why couldn't we let it supervise the `Pooly.PoolSupervisor`s instead? Well, we need something to take the place of `Pool.Server`. In particular, we need a process to start the pools! Now, starting the pool involves: 260 | 261 | * Telling `PoolsSupervisor` to start a `Pooly.PoolSupervisor` child 262 | * `Pooly.PoolSupervisor` will initialize a `Pooly.Server` and a `Pooly.WorkerSupervisor`. 263 | 264 | In order words, this is how we want the design to look like: 265 | 266 | ``` 267 | [Pooly.Supervisor] 268 | / \ 269 | / \ 270 | [Pooly.PoolStarter] [Pooly.PoolsSupervisor] 271 | / | \ 272 | / | [*] 273 | [Pooly.PoolSupervisor] | 274 | / \ [*] 275 | [Pooly.Server] [Pooly.WorkerSupervisor] 276 | ``` 277 | 278 | The `Pooly.PoolStarter` process is a simple GenServer that is stateless, since there is not need for it to keep any. 279 | 280 | 281 | ### Server state 282 | 283 | ```elixir 284 | defmodule State do 285 | defstruct pool_sup: nil, 286 | worker_sup: nil, 287 | monitors: nil, 288 | monitors: nil, 289 | size: nil, 290 | workers: nil, 291 | name: nil, 292 | mfa: nil, 293 | end 294 | ``` 295 | 296 | # Version 4 297 | 298 | ## Features 299 | 300 | * Transactions 301 | * Overflowing of workers 302 | * Waiting and Queuing 303 | 304 | ### Implementing automatic checkout/checkin with transactions 305 | 306 | Up until now, it is the onus of the consumer process to check back in a worker process once it is done with it. However, this is an unreasonable requirement, and we can do better. Just like a database transaction, once the consumer process is done with it, we can automatically have the worker process checked back in. How do we do that? 307 | 308 | ### Blocking and Queuing 309 | 310 | When all workers are busy, a consumer that is willing to wait will be queued up. In this implementation, that is the default behaviour. It is relatively straightforward to implement a non-blocking consumer. (Just have a parameter which says `block`, and if it says `false`, return something like `{:error, full}`) 311 | 312 | For a consumer that blocks, once a worker is checked back into the pool, the consumer is then unblocked and given the worker. 313 | 314 | ### Supporting a variable number of workers 315 | 316 | Next, we want to add some flexibility to `Pooly`. In particular, we want to specify a _maximum overflow_ of workers. What does this buy us? Consider the following scenario. (More research. See Sasa's [article](www.theerlangelist.com/2013/04/parallelizing-independent-tasks.html) on setting size to zero and overflow to 5, essentially for _dynamic_ workers) 317 | 318 | ### Server state 319 | 320 | ```elixir 321 | defmodule State do 322 | defstruct pool_sup: nil, 323 | worker_sup: nil, 324 | monitors: nil, 325 | monitors: nil, 326 | size: nil, 327 | workers: nil, 328 | name: nil, 329 | mfa: nil, 330 | waiting: nil, 331 | overflow: nil, 332 | max_overflow: nil 333 | end 334 | ``` 335 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-2/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for third- 9 | # party users, it should be done in your mix.exs file. 10 | 11 | # Sample configuration: 12 | # 13 | # config :logger, :console, 14 | # level: :info, 15 | # format: "$date $time [$level] $metadata$message\n", 16 | # metadata: [:user_id] 17 | 18 | # It is also possible to import configuration files, relative to this 19 | # directory. For example, you can emulate configuration per environment 20 | # by uncommenting the line below and defining dev.exs, test.exs and such. 21 | # Configuration from the imported file will override the ones defined 22 | # here (which is why it is important to import them last). 23 | # 24 | # import_config "#{Mix.env}.exs" 25 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-2/lib/pooly.ex: -------------------------------------------------------------------------------- 1 | defmodule Pooly do 2 | use Application 3 | 4 | def start(_type, _args) do 5 | pool_config = [mfa: {SampleWorker, :start_link, []}, size: 5] 6 | start_pool(pool_config) 7 | end 8 | 9 | def start_pool(pool_config) do 10 | Pooly.Supervisor.start_link(pool_config) 11 | end 12 | 13 | def checkout do 14 | Pooly.Server.checkout 15 | end 16 | 17 | def checkin(worker_pid) do 18 | Pooly.Server.checkin(worker_pid) 19 | end 20 | 21 | def status do 22 | Pooly.Server.status 23 | end 24 | 25 | end 26 | 27 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-2/lib/pooly/sample_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule SampleWorker do 2 | use GenServer 3 | 4 | def start_link(_) do 5 | GenServer.start_link(__MODULE__, :ok, []) 6 | end 7 | 8 | def stop(pid) do 9 | GenServer.call(pid, :stop) 10 | end 11 | 12 | def handle_call(:stop, _from, state) do 13 | {:stop, :normal, :ok, state} 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-2/lib/pooly/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Pooly.Server do 2 | use GenServer 3 | import Supervisor.Spec 4 | 5 | defmodule State do 6 | defstruct sup: nil, worker_sup: nil, monitors: nil, size: nil, workers: nil, mfa: nil 7 | end 8 | 9 | def start_link(sup, pool_config) do 10 | GenServer.start_link(__MODULE__, [sup, pool_config], name: __MODULE__) 11 | end 12 | 13 | def checkout do 14 | GenServer.call(__MODULE__, :checkout) 15 | end 16 | 17 | def checkin(worker_pid) do 18 | GenServer.cast(__MODULE__, {:checkin, worker_pid}) 19 | end 20 | 21 | def status do 22 | GenServer.call(__MODULE__, :status) 23 | end 24 | 25 | ############# 26 | # Callbacks # 27 | ############# 28 | 29 | def init([sup, pool_config]) when is_pid(sup) do 30 | Process.flag(:trap_exit, true) 31 | monitors = :ets.new(:monitors, [:private]) 32 | init(pool_config, %State{sup: sup, monitors: monitors}) 33 | end 34 | 35 | def init([{:mfa, mfa}|rest], state) do 36 | init(rest, %{state | mfa: mfa}) 37 | end 38 | 39 | def init([{:size, size}|rest], state) do 40 | init(rest, %{state | size: size}) 41 | end 42 | 43 | def init([_|rest], state) do 44 | init(rest, state) 45 | end 46 | 47 | def init([], state) do 48 | send(self(), :start_worker_supervisor) 49 | {:ok, state} 50 | end 51 | 52 | def handle_call(:checkout, {from_pid, _ref}, %{workers: workers, monitors: monitors} = state) do 53 | case workers do 54 | [worker|rest] -> 55 | ref = Process.monitor(from_pid) 56 | true = :ets.insert(monitors, {worker, ref}) 57 | {:reply, worker, %{state | workers: rest}} 58 | 59 | [] -> 60 | {:reply, :noproc, state} 61 | end 62 | end 63 | 64 | def handle_call(:status, _from, %{workers: workers, monitors: monitors} = state) do 65 | {:reply, {length(workers), :ets.info(monitors, :size)}, state} 66 | end 67 | 68 | 69 | def handle_cast({:checkin, worker}, %{workers: workers, monitors: monitors} = state) do 70 | case :ets.lookup(monitors, worker) do 71 | [{pid, ref}] -> 72 | true = Process.demonitor(ref) 73 | true = :ets.delete(monitors, pid) 74 | {:noreply, %{state | workers: [pid|workers]}} 75 | [] -> 76 | {:noreply, state} 77 | end 78 | end 79 | 80 | def handle_info(:start_worker_supervisor, state = %{sup: sup, mfa: mfa, size: size}) do 81 | {:ok, worker_sup} = Supervisor.start_child(sup, supervisor_spec(mfa)) 82 | workers = prepopulate(size, worker_sup) 83 | {:noreply, %{state | worker_sup: worker_sup, workers: workers}} 84 | end 85 | 86 | def handle_info({:DOWN, ref, _, _, _}, state = %{monitors: monitors, workers: workers}) do 87 | case :ets.match(monitors, {:"$1", ref}) do 88 | [[pid]] -> 89 | true = :ets.delete(monitors, pid) 90 | new_state = %{state | workers: [pid|workers]} 91 | {:noreply, new_state} 92 | 93 | [[]] -> 94 | {:noreply, state} 95 | end 96 | end 97 | 98 | def handle_info({:EXIT, pid, _reason}, state = %{monitors: monitors, workers: workers, worker_sup: worker_sup}) do 99 | case :ets.lookup(monitors, pid) do 100 | [{pid, ref}] -> 101 | true = Process.demonitor(ref) 102 | true = :ets.delete(monitors, pid) 103 | new_state = %{state | workers: [new_worker(worker_sup) | List.delete(workers, pid)]} 104 | {:noreply, new_state} 105 | 106 | [] -> 107 | {:noreply, state} 108 | end 109 | end 110 | 111 | ##################### 112 | # Private Functions # 113 | ##################### 114 | 115 | defp prepopulate(size, sup) do 116 | prepopulate(size, sup, []) 117 | end 118 | 119 | defp prepopulate(size, _sup, workers) when size < 1 do 120 | workers 121 | end 122 | 123 | defp prepopulate(size, sup, workers) do 124 | prepopulate(size-1, sup, [new_worker(sup) | workers]) 125 | end 126 | 127 | defp new_worker(sup) do 128 | {:ok, worker} = Supervisor.start_child(sup, [[]]) 129 | Process.link(worker) 130 | worker 131 | end 132 | 133 | defp supervisor_spec(mfa) do 134 | opts = [restart: :temporary] 135 | supervisor(Pooly.WorkerSupervisor, [mfa], opts) 136 | end 137 | 138 | end 139 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-2/lib/pooly/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Pooly.Supervisor do 2 | use Supervisor 3 | 4 | def start_link(pool_config) do 5 | Supervisor.start_link(__MODULE__, pool_config) 6 | end 7 | 8 | def init(pool_config) do 9 | children = [ 10 | worker(Pooly.Server, [self(), pool_config]) 11 | ] 12 | 13 | opts = [strategy: :one_for_all] 14 | 15 | supervise(children, opts) 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-2/lib/pooly/worker_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Pooly.WorkerSupervisor do 2 | use Supervisor 3 | 4 | def start_link({_,_,_} = mfa) do 5 | Supervisor.start_link(__MODULE__, mfa) 6 | end 7 | 8 | def init({m,f,a}) do 9 | worker_opts = [restart: :temporary, 10 | shutdown: 5000, 11 | function: f] 12 | 13 | children = [worker(m, a, worker_opts)] 14 | opts = [strategy: :simple_one_for_one, 15 | max_restarts: 5, 16 | max_seconds: 5] 17 | 18 | supervise(children, opts) 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-2/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Pooly.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :pooly, 6 | version: "0.0.1", 7 | elixir: "~> 1.0", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps()] 11 | end 12 | 13 | def application do 14 | [applications: [:logger], 15 | mod: {Pooly, []}] # hardcoded first. 16 | end 17 | 18 | defp deps do 19 | [] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-2/test/pooly_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PoolyTest do 2 | use ExUnit.Case 3 | 4 | test "the truth" do 5 | assert 1 + 1 == 2 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-2/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-3/README.md: -------------------------------------------------------------------------------- 1 | Pooly 2 | ===== 3 | 4 | ![](http://i.imgur.com/NobRZq0.png) 5 | 6 | > Definition of POOLY 7 | > 8 | > having many pools 9 | > 10 | > – Merriam-Webster Dictionary 11 | 12 | _Pooly_ is a worker pool library inspired by other Erlang worker pool libraries such as [poolboy](https://github.com/devinus/poolboy), [pooler](https://github.com/seth/pooler) and [ppool](http://learnyousomeerlang.com/building-applications-with-otp) (from [Chapter 18](http://learnyousomeerlang.com/building-applications-with-otp) of _Learn You Some Erlang For Great Good_). 13 | 14 | The whole point of this exercise is to make this project a part of the example project in Chapter 6 of the [book](http://www.exotpbook.com). 15 | 16 | ## API 17 | 18 | ```elixir 19 | alias Pooly, as: P 20 | 21 | P.start_link 22 | P.stop 23 | ``` 24 | 25 | ### Starting a pool 26 | 27 | This creates a named pool, attach it to the top-level supervisor and starts a bunch of workers. 28 | 29 | ### Worker Arguments 30 | 31 | ```elixir 32 | pools_config = 33 | [ 34 | [name: "Pool1", 35 | mfa: {SampleWorker, :start_link, []}, 36 | size: 2, 37 | max_overflow: 10 38 | ], 39 | [name: "Pool2", 40 | mfa: {SampleWorker, :start_link, []}, 41 | size: 2, 42 | max_overflow: 0 43 | ], 44 | ] 45 | 46 | 47 | P.start_pool(pool_config) 48 | ``` 49 | 50 | ### Stopping a pool 51 | 52 | ```elixir 53 | P.stop_pool("Pool1") 54 | ``` 55 | 56 | ### Getting workers 57 | 58 | This is the most basic version: 59 | 60 | ```elixir 61 | pid = P.checkout("Pool1") 62 | ``` 63 | 64 | `checkout/3` takes two other arguments. `block` (boolean) and `timeout`. The defaults are `true` and `5000` respectively. If `block` is true, the consumer process will wait for `timeout` milliseconds before timing out. Note that you can pass in `:infinity` as a timeout value. 65 | 66 | For example, this consumer process will wait indefinitely for a process to be available: 67 | 68 | ```elixir 69 | worker_pid = P.checkout("Pool1", true, :infinity) 70 | ``` 71 | 72 | ### Return workers back to the pool 73 | 74 | Returning workers is straightforward: 75 | 76 | ```elixir 77 | P.checkin("Pool1", worker_pid) do 78 | ``` 79 | 80 | ### Getting the status of a pool 81 | 82 | ```elixir 83 | P.status("Pool1") 84 | ``` 85 | 86 | # Version 1 87 | 88 | Although the first version is the most basic, it will get us a pretty long way, since it sets up the framework of the versions to come. 89 | 90 | ## Characteristics 91 | 92 | * Supports a _single_ pool 93 | * Supports a _fixed_ number of workers 94 | * No queuing 95 | * No recovery when consumer and/or worker process fail 96 | 97 | ## Details 98 | 99 | When Pooly first starts (`Pooly.start_link`) it is just this: 100 | 101 | ``` 102 | [Pooly.Supervisor] 103 | / 104 | / 105 | [Pooly.Server] 106 | ``` 107 | 108 | This is because there is no pool to be started yet. To do that: 109 | 110 | ```elixir 111 | Pool.start_pool(:some_worker_pool, {SomeWorker, :start_link, []}) 112 | ``` 113 | 114 | This creates a `Pooly.WorkerSupervisor` with a `:simple_one_for_one` strategy that essentially makes it a `SomeWorker` process factory. This is the _final_ state of the supervision tree, with no workers started yet. 115 | 116 | 117 | ``` 118 | [Pooly.Supervisor] 119 | / \ 120 | / \ 121 | [Pooly.Server] [Pooly.WorkerSupervisor] 122 | ``` 123 | 124 | Next, we need to work on limiting the number of workers. In order for this to happen, we need to introduce another variable. One option we _could_ do is: 125 | 126 | ```elixir 127 | Pool.start_pool(:some_worker_pool, [5, {SomeWorker, :start_link, []]}) 128 | ``` 129 | 130 | However, someone else taking a look at the code (that will be _you_, 2 weeks later) will be left wondering what exactly `5` does. A better way of expressing it would be using a _keyword list_: 131 | 132 | ```elixir 133 | [ 134 | mfa: {SomeWorker, :start_link, []}, 135 | size: 5 136 | ] 137 | ``` 138 | 139 | With a size, we can create a fixed number of workers each time the supervisor starts. Put another way, the supervisor will create `size` number of workers. 140 | 141 | ### Checking out and in workers 142 | 143 | The notion of checking in and out of workers is just like acquiring and releasing of a resource. In this case, the resource is the an available worker, represented by a process id. 144 | 145 | To support this operation, we have the following functions: 146 | 147 | ```elixir 148 | Pooly.checkout # returns an available worker pid, or :noproc if unavailable 149 | ``` 150 | 151 | When done, it the consumer of the worker pid (the process that did the previously check-out) must check the worker pid back in, otherwise, it will cause a resource starvation. An example could be a single process checking out every single worker, and not checking in back to the pool. We will fix this limitation later on. To check-in a worker: 152 | 153 | ```elixir 154 | Pooly.checkin(:some_worker_pool, worker_pid) 155 | ``` 156 | 157 | 158 | ### Server state 159 | 160 | At the end of this iteration, the server state should look like: 161 | 162 | ```elixir 163 | defmodule State do 164 | defstruct supervisor: nil, 165 | workers: [], 166 | monitors: :ets.new(:monitors, [:private]), 167 | size: 5 168 | end 169 | ``` 170 | 171 | ### Running it 172 | 173 | __TODO:__ _Create a sample worker and put `Pooly` through its paces_ 174 | 175 | # Version 2 176 | 177 | ## Characteristics 178 | 179 | * Supports a _single_ pool 180 | * Supports a _fixed_ number of workers 181 | * No queuing 182 | * recovery when consumer and/or worker process fail 183 | 184 | ### Linking 185 | 186 | Besides checking in a worker, the worker could crash too. Othertimes, the worker could exit normally. Since the supervisor stance on restarting crashed workers is `:temporary`, this means that workers are never restarted. That's because in general we never know whether a worker should be restarted. While you can build this into the implementation like having it as a setting, we will keep it simple. 187 | 188 | In order to handle these various situations, we need to know when something happens to a checked out worker process. Our worker processes should crash too if the server crashes. Links (and trapping exits) are perfect for this. What should happen when a worker crashes? Well, the pool should automatically create a new worker, no questions asked. 189 | 190 | ### Monitoring 191 | 192 | How do we know when a consumer process dies? Monitors! What should happen then when we detect that a consumer process dies? How can we retrieve the worker? (Monitor reference!) 193 | 194 | # Version 3 195 | 196 | ## Characteristics 197 | 198 | * Supports _multiple_ pools by dynamically creating supervisors 199 | * Supports a _variable_ number of workers 200 | 201 | So far, our worker pool can only handle one pool, which isn't terribly useful. In this iteration, we will add support for multiple pools and finally add more bells and whistles (be specific about this). 202 | 203 | ### Supporting multiple queues 204 | 205 | The most straight forward way would be to design the supervision tree like so: 206 | 207 | ``` 208 | [Pooly.Supervisor] 209 | / / | \ 210 | / / | \ 211 | [Pooly.Server] | \ 212 | / | \ 213 | / | [Pooly.WorkerSupervisor] 214 | [Pooly.WorkerSupervisor] 215 | | 216 | [Pooly.WorkerSupervisor] 217 | ``` 218 | 219 | ### Error Kernels and Error Isolation 220 | 221 | We are essentially sticking more `WorkerSupervisor`'s into `Pooly.Supervisor`. This is a bad design. The issue here is the _error kernel_. Issues with any of the `WorkerSupervisor`s shouldn't affect the `Pooly.Server`. (More reasons needed, separation of concerns). It pays to think about what happens when a process crashes and who gets affected. 222 | 223 | 224 | The fix is to add another supervisor to handle all the worker supervisors, say a `Pooly.WorkersSupervisor`. This _might_ design we are shooting for: 225 | 226 | ``` 227 | [Pooly.Supervisor] 228 | / \ 229 | / \ 230 | [Pooly.Server] [Pooly.WorkersSupervisor] 231 | / | \ 232 | / | [Pooly.WorkerSupervisor] 233 | [Pooly.WorkerSupervisor] | 234 | [Pooly.WorkerSupervisor] 235 | ``` 236 | 237 | Do you notice another problem? 238 | 239 | Currently, the poor `Pooly.Server` process has to handle every request that is meant for _any_ pool. 240 | 241 | This means that the lone server process might pose a bottle neck if messages to it come fast and furious, and could potentially flood it's mailbox. `Pooly.Server` also presents a single point of failure, since it contains the state of _every_ pool, and having the server process dead renders the pools useless. 242 | 243 | The simplest thing to do is to have a dedicated `Pool.Server` process for each pool. So let's stick `Pooly.Server` into `Pool.WorkersSupervisor` ok? Not ok! If we did that, then `WorkersSupervisor` is going to have to supervise `Pool.Server`s too. 244 | 245 | > A good exercise would be to quickly sketch out how you think the supervision tree should look like. 246 | 247 | ``` 248 | [Pooly.Supervisor] 249 | \ 250 | \ 251 | [Pooly.PoolsSupervisor] 252 | / | \ 253 | / | [*] 254 | [Pooly.PoolSupervisor] | 255 | / \ [*] 256 | [Pooly.Server] [Pooly.WorkerSupervisor] 257 | ``` 258 | 259 | You might find it slightly weird (I do!) that the `Pooly.Supervisor` is only supervising one child. Why couldn't we let it supervise the `Pooly.PoolSupervisor`s instead? Well, we need something to take the place of `Pool.Server`. In particular, we need a process to start the pools! Now, starting the pool involves: 260 | 261 | * Telling `PoolsSupervisor` to start a `Pooly.PoolSupervisor` child 262 | * `Pooly.PoolSupervisor` will initialize a `Pooly.Server` and a `Pooly.WorkerSupervisor`. 263 | 264 | In order words, this is how we want the design to look like: 265 | 266 | ``` 267 | [Pooly.Supervisor] 268 | / \ 269 | / \ 270 | [Pooly.PoolStarter] [Pooly.PoolsSupervisor] 271 | / | \ 272 | / | [*] 273 | [Pooly.PoolSupervisor] | 274 | / \ [*] 275 | [Pooly.Server] [Pooly.WorkerSupervisor] 276 | ``` 277 | 278 | The `Pooly.PoolStarter` process is a simple GenServer that is stateless, since there is not need for it to keep any. 279 | 280 | 281 | ### Server state 282 | 283 | ```elixir 284 | defmodule State do 285 | defstruct pool_sup: nil, 286 | worker_sup: nil, 287 | monitors: nil, 288 | monitors: nil, 289 | size: nil, 290 | workers: nil, 291 | name: nil, 292 | mfa: nil, 293 | end 294 | ``` 295 | 296 | # Version 4 297 | 298 | ## Features 299 | 300 | * Transactions 301 | * Overflowing of workers 302 | * Waiting and Queuing 303 | 304 | ### Implementing automatic checkout/checkin with transactions 305 | 306 | Up until now, it is the onus of the consumer process to check back in a worker process once it is done with it. However, this is an unreasonable requirement, and we can do better. Just like a database transaction, once the consumer process is done with it, we can automatically have the worker process checked back in. How do we do that? 307 | 308 | ### Blocking and Queuing 309 | 310 | When all workers are busy, a consumer that is willing to wait will be queued up. In this implementation, that is the default behaviour. It is relatively straightforward to implement a non-blocking consumer. (Just have a parameter which says `block`, and if it says `false`, return something like `{:error, full}`) 311 | 312 | For a consumer that blocks, once a worker is checked back into the pool, the consumer is then unblocked and given the worker. 313 | 314 | ### Supporting a variable number of workers 315 | 316 | Next, we want to add some flexibility to `Pooly`. In particular, we want to specify a _maximum overflow_ of workers. What does this buy us? Consider the following scenario. (More research. See Sasa's [article](www.theerlangelist.com/2013/04/parallelizing-independent-tasks.html) on setting size to zero and overflow to 5, essentially for _dynamic_ workers) 317 | 318 | ### Server state 319 | 320 | ```elixir 321 | defmodule State do 322 | defstruct pool_sup: nil, 323 | worker_sup: nil, 324 | monitors: nil, 325 | monitors: nil, 326 | size: nil, 327 | workers: nil, 328 | name: nil, 329 | mfa: nil, 330 | waiting: nil, 331 | overflow: nil, 332 | max_overflow: nil 333 | end 334 | ``` 335 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-3/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for third- 9 | # party users, it should be done in your mix.exs file. 10 | 11 | # Sample configuration: 12 | # 13 | # config :logger, :console, 14 | # level: :info, 15 | # format: "$date $time [$level] $metadata$message\n", 16 | # metadata: [:user_id] 17 | 18 | # It is also possible to import configuration files, relative to this 19 | # directory. For example, you can emulate configuration per environment 20 | # by uncommenting the line below and defining dev.exs, test.exs and such. 21 | # Configuration from the imported file will override the ones defined 22 | # here (which is why it is important to import them last). 23 | # 24 | # import_config "#{Mix.env}.exs" 25 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-3/lib/pooly.ex: -------------------------------------------------------------------------------- 1 | defmodule Pooly do 2 | use Application 3 | 4 | def start(_type, _args) do 5 | pools_config = 6 | [ 7 | [name: "Pool1", mfa: {SampleWorker, :start_link, []}, size: 2], 8 | [name: "Pool2", mfa: {SampleWorker, :start_link, []}, size: 3], 9 | [name: "Pool3", mfa: {SampleWorker, :start_link, []}, size: 4], 10 | ] 11 | 12 | start_pools(pools_config) 13 | end 14 | 15 | def start_pools(pools_config) do 16 | Pooly.Supervisor.start_link(pools_config) 17 | end 18 | 19 | def checkout(pool_name) do 20 | Pooly.Server.checkout(pool_name) 21 | end 22 | 23 | def checkin(pool_name, worker_pid) do 24 | Pooly.Server.checkin(pool_name, worker_pid) 25 | end 26 | 27 | def status(pool_name) do 28 | Pooly.Server.status(pool_name) 29 | end 30 | 31 | end 32 | 33 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-3/lib/pooly/pool_server.ex: -------------------------------------------------------------------------------- 1 | defmodule Pooly.PoolServer do 2 | use GenServer 3 | import Supervisor.Spec 4 | 5 | defmodule State do 6 | defstruct pool_sup: nil, worker_sup: nil, monitors: nil, size: nil, workers: nil, name: nil, mfa: nil 7 | end 8 | 9 | def start_link(pool_sup, pool_config) do 10 | GenServer.start_link(__MODULE__, [pool_sup, pool_config], name: name(pool_config[:name])) 11 | end 12 | 13 | def checkout(pool_name) do 14 | GenServer.call(name(pool_name), :checkout) 15 | end 16 | 17 | def checkin(pool_name, worker_pid) do 18 | GenServer.cast(name(pool_name), {:checkin, worker_pid}) 19 | end 20 | 21 | def status(pool_name) do 22 | GenServer.call(name(pool_name), :status) 23 | end 24 | 25 | def terminate(_reason, _state) do 26 | :ok 27 | end 28 | 29 | ############# 30 | # Callbacks # 31 | ############j 32 | 33 | def init([pool_sup, pool_config]) when is_pid(pool_sup) do 34 | Process.flag(:trap_exit, true) 35 | monitors = :ets.new(:monitors, [:private]) 36 | init(pool_config, %State{pool_sup: pool_sup, monitors: monitors}) 37 | end 38 | 39 | def init([{:name, name}|rest], state) do 40 | init(rest, %{state | name: name}) 41 | end 42 | 43 | def init([{:mfa, mfa}|rest], state) do 44 | init(rest, %{state | mfa: mfa}) 45 | end 46 | 47 | def init([{:size, size}|rest], state) do 48 | init(rest, %{state | size: size}) 49 | end 50 | 51 | def init([], state) do 52 | send(self(), :start_worker_supervisor) 53 | {:ok, state} 54 | end 55 | 56 | def init([_|rest], state) do 57 | init(rest, state) 58 | end 59 | 60 | def handle_call(:checkout, {from_pid, _ref}, %{workers: workers, monitors: monitors} = state) do 61 | case workers do 62 | [worker|rest] -> 63 | ref = Process.monitor(from_pid) 64 | true = :ets.insert(monitors, {worker, ref}) 65 | {:reply, worker, %{state | workers: rest}} 66 | 67 | [] -> 68 | {:reply, :noproc, state} 69 | end 70 | end 71 | 72 | def handle_call(:status, _from, %{workers: workers, monitors: monitors} = state) do 73 | {:reply, {length(workers), :ets.info(monitors, :size)}, state} 74 | end 75 | 76 | def handle_cast({:checkin, worker}, %{workers: workers, monitors: monitors} = state) do 77 | case :ets.lookup(monitors, worker) do 78 | [{pid, ref}] -> 79 | true = Process.demonitor(ref) 80 | true = :ets.delete(monitors, pid) 81 | {:noreply, %{state | workers: [pid|workers]}} 82 | [] -> 83 | {:noreply, state} 84 | end 85 | end 86 | 87 | def handle_info(:start_worker_supervisor, state = %{pool_sup: pool_sup, name: name, mfa: mfa, size: size}) do 88 | {:ok, worker_sup} = Supervisor.start_child(pool_sup, supervisor_spec(name, mfa)) 89 | workers = prepopulate(size, worker_sup) 90 | {:noreply, %{state | worker_sup: worker_sup, workers: workers}} 91 | end 92 | 93 | def handle_info({:DOWN, ref, _, _, _}, state = %{monitors: monitors, workers: workers}) do 94 | case :ets.match(monitors, {:"$1", ref}) do 95 | [[pid]] -> 96 | true = :ets.delete(monitors, pid) 97 | new_state = %{state | workers: [pid|workers]} 98 | {:noreply, new_state} 99 | 100 | [[]] -> 101 | {:noreply, state} 102 | end 103 | end 104 | 105 | def handle_info({:EXIT, worker_sup, reason}, state = %{worker_sup: worker_sup}) do 106 | {:stop, reason, state} 107 | end 108 | 109 | def handle_info({:EXIT, pid, _reason}, state = %{monitors: monitors, workers: workers, worker_sup: worker_sup}) do 110 | case :ets.lookup(monitors, pid) do 111 | [{pid, ref}] -> 112 | true = Process.demonitor(ref) 113 | true = :ets.delete(monitors, pid) 114 | new_state = %{state | workers: [new_worker(worker_sup)|workers]} 115 | {:noreply, new_state} 116 | 117 | _ -> 118 | {:noreply, state} 119 | end 120 | end 121 | 122 | ##################### 123 | # Private Functions # 124 | ##################### 125 | 126 | defp name(pool_name) do 127 | :"#{pool_name}Server" 128 | end 129 | 130 | defp prepopulate(size, sup) do 131 | prepopulate(size, sup, []) 132 | end 133 | 134 | defp prepopulate(size, _sup, workers) when size < 1 do 135 | workers 136 | end 137 | 138 | defp prepopulate(size, sup, workers) do 139 | prepopulate(size-1, sup, [new_worker(sup) | workers]) 140 | end 141 | 142 | defp new_worker(sup) do 143 | {:ok, worker} = Supervisor.start_child(sup, [[]]) 144 | Process.link(worker) 145 | worker 146 | end 147 | 148 | defp supervisor_spec(name, mfa) do 149 | # NOTE: The reason this is set to temporary is because the WorkerSupervisor 150 | # is started by the PoolServer. 151 | opts = [id: name <> "WorkerSupervisor", shutdown: 10000, restart: :temporary] 152 | supervisor(Pooly.WorkerSupervisor, [self(), mfa], opts) 153 | end 154 | 155 | end 156 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-3/lib/pooly/pool_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Pooly.PoolSupervisor do 2 | use Supervisor 3 | 4 | def start_link(pool_config) do 5 | # NOTE: :name must be an atom! 6 | Supervisor.start_link(__MODULE__, pool_config) 7 | end 8 | 9 | def init(pool_config) do 10 | opts = [ 11 | strategy: :one_for_all 12 | ] 13 | 14 | children = [ 15 | worker(Pooly.PoolServer, [self(), pool_config]) 16 | ] 17 | 18 | supervise(children, opts) 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-3/lib/pooly/pools_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Pooly.PoolsSupervisor do 2 | use Supervisor 3 | 4 | def start_link do 5 | Supervisor.start_link(__MODULE__, [], name: __MODULE__) 6 | end 7 | 8 | def init(_) do 9 | opts = [ 10 | strategy: :one_for_one 11 | ] 12 | 13 | supervise([], opts) 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-3/lib/pooly/sample_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule SampleWorker do 2 | use GenServer 3 | 4 | def start_link(_) do 5 | GenServer.start_link(__MODULE__, :ok, []) 6 | end 7 | 8 | def stop(pid) do 9 | GenServer.call(pid, :stop) 10 | end 11 | 12 | def handle_call(:stop, _from, state) do 13 | {:stop, :normal, :ok, state} 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-3/lib/pooly/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Pooly.Server do 2 | use GenServer 3 | import Supervisor.Spec 4 | 5 | ####### 6 | # API # 7 | ####### 8 | 9 | def start_link(pools_config) do 10 | GenServer.start_link(__MODULE__, pools_config, name: __MODULE__) 11 | end 12 | 13 | def checkout(pool_name) do 14 | GenServer.call(:"#{pool_name}Server", :checkout) 15 | end 16 | 17 | def checkin(pool_name, worker_pid) do 18 | GenServer.cast(:"#{pool_name}Server", {:checkin, worker_pid}) 19 | end 20 | 21 | def status(pool_name) do 22 | GenServer.call(:"#{pool_name}Server", :status) 23 | end 24 | 25 | ############# 26 | # Callbacks # 27 | ############# 28 | 29 | def init(pools_config) do 30 | pools_config |> Enum.each(fn(pool_config) -> 31 | send(self(), {:start_pool, pool_config}) 32 | end) 33 | 34 | {:ok, pools_config} 35 | end 36 | 37 | def handle_info({:start_pool, pool_config}, state) do 38 | {:ok, _pool_sup} = Supervisor.start_child(Pooly.PoolsSupervisor, supervisor_spec(pool_config)) 39 | {:noreply, state} 40 | end 41 | 42 | ##################### 43 | # Private Functions # 44 | ##################### 45 | 46 | defp supervisor_spec(pool_config) do 47 | opts = [id: :"#{pool_config[:name]}Supervisor"] 48 | supervisor(Pooly.PoolSupervisor, [pool_config], opts) 49 | end 50 | 51 | end 52 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-3/lib/pooly/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Pooly.Supervisor do 2 | use Supervisor 3 | 4 | def start_link(pools_config) do 5 | Supervisor.start_link(__MODULE__, pools_config, name: __MODULE__) 6 | end 7 | 8 | def init(pools_config) do 9 | children = [ 10 | supervisor(Pooly.PoolsSupervisor, []), 11 | worker(Pooly.Server, [pools_config]) 12 | ] 13 | 14 | opts = [strategy: :one_for_all, 15 | max_restart: 1, 16 | max_time: 3600] 17 | 18 | supervise(children, opts) 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-3/lib/pooly/worker_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Pooly.WorkerSupervisor do 2 | use Supervisor 3 | 4 | def start_link(pool_server, {_,_,_} = mfa) do 5 | Supervisor.start_link(__MODULE__, [pool_server, mfa]) 6 | end 7 | 8 | def init([pool_server, {m,f,a}]) do 9 | Process.link(pool_server) 10 | worker_opts = [restart: :temporary, 11 | shutdown: 5000, 12 | function: f] 13 | 14 | children = [worker(m, a, worker_opts)] 15 | opts = [strategy: :simple_one_for_one, 16 | max_restarts: 5, 17 | max_seconds: 5] 18 | 19 | supervise(children, opts) 20 | end 21 | 22 | end 23 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-3/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Pooly.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :pooly, 6 | version: "0.0.1", 7 | elixir: "~> 1.0", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps()] 11 | end 12 | 13 | def application do 14 | [applications: [:logger], 15 | mod: {Pooly, []}] # hardcoded first. 16 | end 17 | 18 | defp deps do 19 | [] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-3/test/pooly_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PoolyTest do 2 | use ExUnit.Case 3 | 4 | test "the truth" do 5 | assert 1 + 1 == 2 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-3/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-4/README.md: -------------------------------------------------------------------------------- 1 | Pooly 2 | ===== 3 | 4 | ![](http://i.imgur.com/NobRZq0.png) 5 | 6 | > Definition of POOLY 7 | > 8 | > having many pools 9 | > 10 | > – Merriam-Webster Dictionary 11 | 12 | _Pooly_ is a worker pool library inspired by other Erlang worker pool libraries such as [poolboy](https://github.com/devinus/poolboy), [pooler](https://github.com/seth/pooler) and [ppool](http://learnyousomeerlang.com/building-applications-with-otp) (from [Chapter 18](http://learnyousomeerlang.com/building-applications-with-otp) of _Learn You Some Erlang For Great Good_). 13 | 14 | The whole point of this exercise is to make this project a part of the example project in Chapter 6 of the [book](http://www.exotpbook.com). 15 | 16 | ## API 17 | 18 | ```elixir 19 | alias Pooly, as: P 20 | 21 | P.start_link 22 | P.stop 23 | ``` 24 | 25 | ### Starting a pool 26 | 27 | This creates a named pool, attach it to the top-level supervisor and starts a bunch of workers. 28 | 29 | ### Worker Arguments 30 | 31 | ```elixir 32 | pools_config = 33 | [ 34 | [name: "Pool1", 35 | mfa: {SampleWorker, :start_link, []}, 36 | size: 2, 37 | max_overflow: 10 38 | ], 39 | [name: "Pool2", 40 | mfa: {SampleWorker, :start_link, []}, 41 | size: 2, 42 | max_overflow: 0 43 | ], 44 | ] 45 | 46 | 47 | P.start_pool(pool_config) 48 | ``` 49 | 50 | ### Stopping a pool 51 | 52 | ```elixir 53 | P.stop_pool("Pool1") 54 | ``` 55 | 56 | ### Getting workers 57 | 58 | This is the most basic version: 59 | 60 | ```elixir 61 | pid = P.checkout("Pool1") 62 | ``` 63 | 64 | `checkout/3` takes two other arguments. `block` (boolean) and `timeout`. The defaults are `true` and `5000` respectively. If `block` is true, the consumer process will wait for `timeout` milliseconds before timing out. Note that you can pass in `:infinity` as a timeout value. 65 | 66 | For example, this consumer process will wait indefinitely for a process to be available: 67 | 68 | ```elixir 69 | worker_pid = P.checkout("Pool1", true, :infinity) 70 | ``` 71 | 72 | ### Return workers back to the pool 73 | 74 | Returning workers is straightforward: 75 | 76 | ```elixir 77 | P.checkin("Pool1", worker_pid) do 78 | ``` 79 | 80 | ### Getting the status of a pool 81 | 82 | ```elixir 83 | P.status("Pool1") 84 | ``` 85 | 86 | # Version 1 87 | 88 | Although the first version is the most basic, it will get us a pretty long way, since it sets up the framework of the versions to come. 89 | 90 | ## Characteristics 91 | 92 | * Supports a _single_ pool 93 | * Supports a _fixed_ number of workers 94 | * No queuing 95 | * No recovery when consumer and/or worker process fail 96 | 97 | ## Details 98 | 99 | When Pooly first starts (`Pooly.start_link`) it is just this: 100 | 101 | ``` 102 | [Pooly.Supervisor] 103 | / 104 | / 105 | [Pooly.Server] 106 | ``` 107 | 108 | This is because there is no pool to be started yet. To do that: 109 | 110 | ```elixir 111 | Pool.start_pool(:some_worker_pool, {SomeWorker, :start_link, []}) 112 | ``` 113 | 114 | This creates a `Pooly.WorkerSupervisor` with a `:simple_one_for_one` strategy that essentially makes it a `SomeWorker` process factory. This is the _final_ state of the supervision tree, with no workers started yet. 115 | 116 | 117 | ``` 118 | [Pooly.Supervisor] 119 | / \ 120 | / \ 121 | [Pooly.Server] [Pooly.WorkerSupervisor] 122 | ``` 123 | 124 | Next, we need to work on limiting the number of workers. In order for this to happen, we need to introduce another variable. One option we _could_ do is: 125 | 126 | ```elixir 127 | Pool.start_pool(:some_worker_pool, [5, {SomeWorker, :start_link, []]}) 128 | ``` 129 | 130 | However, someone else taking a look at the code (that will be _you_, 2 weeks later) will be left wondering what exactly `5` does. A better way of expressing it would be using a _keyword list_: 131 | 132 | ```elixir 133 | [ 134 | mfa: {SomeWorker, :start_link, []}, 135 | size: 5 136 | ] 137 | ``` 138 | 139 | With a size, we can create a fixed number of workers each time the supervisor starts. Put another way, the supervisor will create `size` number of workers. 140 | 141 | ### Checking out and in workers 142 | 143 | The notion of checking in and out of workers is just like acquiring and releasing of a resource. In this case, the resource is the an available worker, represented by a process id. 144 | 145 | To support this operation, we have the following functions: 146 | 147 | ```elixir 148 | Pooly.checkout # returns an available worker pid, or :noproc if unavailable 149 | ``` 150 | 151 | When done, it the consumer of the worker pid (the process that did the previously check-out) must check the worker pid back in, otherwise, it will cause a resource starvation. An example could be a single process checking out every single worker, and not checking in back to the pool. We will fix this limitation later on. To check-in a worker: 152 | 153 | ```elixir 154 | Pooly.checkin(:some_worker_pool, worker_pid) 155 | ``` 156 | 157 | 158 | ### Server state 159 | 160 | At the end of this iteration, the server state should look like: 161 | 162 | ```elixir 163 | defmodule State do 164 | defstruct supervisor: nil, 165 | workers: [], 166 | monitors: :ets.new(:monitors, [:private]), 167 | size: 5 168 | end 169 | ``` 170 | 171 | ### Running it 172 | 173 | __TODO:__ _Create a sample worker and put `Pooly` through its paces_ 174 | 175 | # Version 2 176 | 177 | ## Characteristics 178 | 179 | * Supports a _single_ pool 180 | * Supports a _fixed_ number of workers 181 | * No queuing 182 | * recovery when consumer and/or worker process fail 183 | 184 | ### Linking 185 | 186 | Besides checking in a worker, the worker could crash too. Othertimes, the worker could exit normally. Since the supervisor stance on restarting crashed workers is `:temporary`, this means that workers are never restarted. That's because in general we never know whether a worker should be restarted. While you can build this into the implementation like having it as a setting, we will keep it simple. 187 | 188 | In order to handle these various situations, we need to know when something happens to a checked out worker process. Our worker processes should crash too if the server crashes. Links (and trapping exits) are perfect for this. What should happen when a worker crashes? Well, the pool should automatically create a new worker, no questions asked. 189 | 190 | ### Monitoring 191 | 192 | How do we know when a consumer process dies? Monitors! What should happen then when we detect that a consumer process dies? How can we retrieve the worker? (Monitor reference!) 193 | 194 | # Version 3 195 | 196 | ## Characteristics 197 | 198 | * Supports _multiple_ pools by dynamically creating supervisors 199 | * Supports a _variable_ number of workers 200 | 201 | So far, our worker pool can only handle one pool, which isn't terribly useful. In this iteration, we will add support for multiple pools and finally add more bells and whistles (be specific about this). 202 | 203 | ### Supporting multiple queues 204 | 205 | The most straight forward way would be to design the supervision tree like so: 206 | 207 | ``` 208 | [Pooly.Supervisor] 209 | / / | \ 210 | / / | \ 211 | [Pooly.Server] | \ 212 | / | \ 213 | / | [Pooly.WorkerSupervisor] 214 | [Pooly.WorkerSupervisor] 215 | | 216 | [Pooly.WorkerSupervisor] 217 | ``` 218 | 219 | ### Error Kernels and Error Isolation 220 | 221 | We are essentially sticking more `WorkerSupervisor`'s into `Pooly.Supervisor`. This is a bad design. The issue here is the _error kernel_. Issues with any of the `WorkerSupervisor`s shouldn't affect the `Pooly.Server`. (More reasons needed, separation of concerns). It pays to think about what happens when a process crashes and who gets affected. 222 | 223 | 224 | The fix is to add another supervisor to handle all the worker supervisors, say a `Pooly.WorkersSupervisor`. This _might_ design we are shooting for: 225 | 226 | ``` 227 | [Pooly.Supervisor] 228 | / \ 229 | / \ 230 | [Pooly.Server] [Pooly.WorkersSupervisor] 231 | / | \ 232 | / | [Pooly.WorkerSupervisor] 233 | [Pooly.WorkerSupervisor] | 234 | [Pooly.WorkerSupervisor] 235 | ``` 236 | 237 | Do you notice another problem? 238 | 239 | Currently, the poor `Pooly.Server` process has to handle every request that is meant for _any_ pool. 240 | 241 | This means that the lone server process might pose a bottle neck if messages to it come fast and furious, and could potentially flood it's mailbox. `Pooly.Server` also presents a single point of failure, since it contains the state of _every_ pool, and having the server process dead renders the pools useless. 242 | 243 | The simplest thing to do is to have a dedicated `Pool.Server` process for each pool. So let's stick `Pooly.Server` into `Pool.WorkersSupervisor` ok? Not ok! If we did that, then `WorkersSupervisor` is going to have to supervise `Pool.Server`s too. 244 | 245 | > A good exercise would be to quickly sketch out how you think the supervision tree should look like. 246 | 247 | ``` 248 | [Pooly.Supervisor] 249 | \ 250 | \ 251 | [Pooly.PoolsSupervisor] 252 | / | \ 253 | / | [*] 254 | [Pooly.PoolSupervisor] | 255 | / \ [*] 256 | [Pooly.Server] [Pooly.WorkerSupervisor] 257 | ``` 258 | 259 | You might find it slightly weird (I do!) that the `Pooly.Supervisor` is only supervising one child. Why couldn't we let it supervise the `Pooly.PoolSupervisor`s instead? Well, we need something to take the place of `Pool.Server`. In particular, we need a process to start the pools! Now, starting the pool involves: 260 | 261 | * Telling `PoolsSupervisor` to start a `Pooly.PoolSupervisor` child 262 | * `Pooly.PoolSupervisor` will initialize a `Pooly.Server` and a `Pooly.WorkerSupervisor`. 263 | 264 | In order words, this is how we want the design to look like: 265 | 266 | ``` 267 | [Pooly.Supervisor] 268 | / \ 269 | / \ 270 | [Pooly.PoolStarter] [Pooly.PoolsSupervisor] 271 | / | \ 272 | / | [*] 273 | [Pooly.PoolSupervisor] | 274 | / \ [*] 275 | [Pooly.Server] [Pooly.WorkerSupervisor] 276 | ``` 277 | 278 | The `Pooly.PoolStarter` process is a simple GenServer that is stateless, since there is not need for it to keep any. 279 | 280 | 281 | ### Server state 282 | 283 | ```elixir 284 | defmodule State do 285 | defstruct pool_sup: nil, 286 | worker_sup: nil, 287 | monitors: nil, 288 | monitors: nil, 289 | size: nil, 290 | workers: nil, 291 | name: nil, 292 | mfa: nil, 293 | end 294 | ``` 295 | 296 | # Version 4 297 | 298 | ## Features 299 | 300 | * Transactions 301 | * Overflowing of workers 302 | * Waiting and Queuing 303 | 304 | ### Implementing automatic checkout/checkin with transactions 305 | 306 | Up until now, it is the onus of the consumer process to check back in a worker process once it is done with it. However, this is an unreasonable requirement, and we can do better. Just like a database transaction, once the consumer process is done with it, we can automatically have the worker process checked back in. How do we do that? 307 | 308 | ### Blocking and Queuing 309 | 310 | When all workers are busy, a consumer that is willing to wait will be queued up. In this implementation, that is the default behaviour. It is relatively straightforward to implement a non-blocking consumer. (Just have a parameter which says `block`, and if it says `false`, return something like `{:error, full}`) 311 | 312 | For a consumer that blocks, once a worker is checked back into the pool, the consumer is then unblocked and given the worker. 313 | 314 | ### Supporting a variable number of workers 315 | 316 | Next, we want to add some flexibility to `Pooly`. In particular, we want to specify a _maximum overflow_ of workers. What does this buy us? Consider the following scenario. (More research. See Sasa's [article](www.theerlangelist.com/2013/04/parallelizing-independent-tasks.html) on setting size to zero and overflow to 5, essentially for _dynamic_ workers) 317 | 318 | ### Server state 319 | 320 | ```elixir 321 | defmodule State do 322 | defstruct pool_sup: nil, 323 | worker_sup: nil, 324 | monitors: nil, 325 | monitors: nil, 326 | size: nil, 327 | workers: nil, 328 | name: nil, 329 | mfa: nil, 330 | waiting: nil, 331 | overflow: nil, 332 | max_overflow: nil 333 | end 334 | ``` 335 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-4/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for third- 9 | # party users, it should be done in your mix.exs file. 10 | 11 | # Sample configuration: 12 | # 13 | # config :logger, :console, 14 | # level: :info, 15 | # format: "$date $time [$level] $metadata$message\n", 16 | # metadata: [:user_id] 17 | 18 | # It is also possible to import configuration files, relative to this 19 | # directory. For example, you can emulate configuration per environment 20 | # by uncommenting the line below and defining dev.exs, test.exs and such. 21 | # Configuration from the imported file will override the ones defined 22 | # here (which is why it is important to import them last). 23 | # 24 | # import_config "#{Mix.env}.exs" 25 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-4/lib/pooly.ex: -------------------------------------------------------------------------------- 1 | defmodule Pooly do 2 | use Application 3 | 4 | @timeout 5000 5 | 6 | def start(_type, _args) do 7 | pools_config = 8 | [ 9 | [name: "Pool1", 10 | mfa: {SampleWorker, :start_link, []}, 11 | size: 2, 12 | max_overflow: 1 13 | ], 14 | [name: "Pool2", 15 | mfa: {SampleWorker, :start_link, []}, 16 | size: 3, 17 | max_overflow: 0 18 | ], 19 | [name: "Pool3", 20 | mfa: {SampleWorker, :start_link, []}, 21 | size: 4, 22 | max_overflow: 0 23 | ], 24 | ] 25 | 26 | start_pools(pools_config) 27 | end 28 | 29 | def start_pools(pools_config) do 30 | Pooly.Supervisor.start_link(pools_config) 31 | end 32 | 33 | def checkout(pool_name, block \\ true, timeout \\ @timeout) do 34 | Pooly.Server.checkout(pool_name, block, timeout) 35 | end 36 | 37 | def checkin(pool_name, worker_pid) do 38 | Pooly.Server.checkin(pool_name, worker_pid) 39 | end 40 | 41 | def transaction(pool_name, fun, timeout) do 42 | Pooly.Server.transaction(pool_name, fun, timeout) 43 | end 44 | 45 | def status(pool_name) do 46 | Pooly.Server.status(pool_name) 47 | end 48 | 49 | end 50 | 51 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-4/lib/pooly/pool_server.ex: -------------------------------------------------------------------------------- 1 | defmodule Pooly.PoolServer do 2 | use GenServer 3 | import Supervisor.Spec 4 | 5 | defmodule State do 6 | defstruct pool_sup: nil, 7 | worker_sup: nil, 8 | monitors: nil, 9 | monitors: nil, 10 | size: nil, 11 | workers: nil, 12 | name: nil, 13 | mfa: nil, 14 | waiting: nil, 15 | overflow: nil, 16 | max_overflow: nil 17 | end 18 | 19 | def start_link(pool_sup, pool_config) do 20 | GenServer.start_link(__MODULE__, [pool_sup, pool_config], name: name(pool_config[:name])) 21 | end 22 | 23 | def checkout(pool_name, block, timeout) do 24 | GenServer.call(name(pool_name), {:checkout, block}, timeout) 25 | end 26 | 27 | def checkin(pool_name, worker_pid) do 28 | GenServer.cast(name(pool_name), {:checkin, worker_pid}) 29 | end 30 | 31 | def status(pool_name) do 32 | GenServer.call(name(pool_name), :status) 33 | end 34 | 35 | ############# 36 | # Callbacks # 37 | ############j 38 | 39 | def init([pool_sup, pool_config]) when is_pid(pool_sup) do 40 | Process.flag(:trap_exit, true) 41 | monitors = :ets.new(:monitors, [:private]) 42 | waiting = :queue.new 43 | state = %State{pool_sup: pool_sup, monitors: monitors, waiting: waiting, overflow: 0} 44 | 45 | init(pool_config, state) 46 | end 47 | 48 | def init([{:name, name}|rest], state) do 49 | init(rest, %{state | name: name}) 50 | end 51 | 52 | def init([{:mfa, mfa}|rest], state) do 53 | init(rest, %{state | mfa: mfa}) 54 | end 55 | 56 | def init([{:size, size}|rest], state) do 57 | init(rest, %{state | size: size}) 58 | end 59 | 60 | def init([{:max_overflow, max_overflow}|rest], state) do 61 | init(rest, %{state | max_overflow: max_overflow}) 62 | end 63 | 64 | def init([], state) do 65 | send(self(), :start_worker_supervisor) 66 | {:ok, state} 67 | end 68 | 69 | def init([_|rest], state) do 70 | init(rest, state) 71 | end 72 | 73 | def handle_call({:checkout, block}, {from_pid, _ref} = from, state) do 74 | # NOTE: you _cannot_ write state = %{workers: worker} 75 | %{worker_sup: worker_sup, 76 | workers: workers, 77 | monitors: monitors, 78 | waiting: waiting, 79 | overflow: overflow, 80 | max_overflow: max_overflow} = state 81 | 82 | case workers do 83 | [worker|rest] -> 84 | ref = Process.monitor(from_pid) 85 | true = :ets.insert(monitors, {worker, ref}) 86 | {:reply, worker, %{state | workers: rest}} 87 | 88 | [] when max_overflow > 0 and overflow < max_overflow -> 89 | {worker, ref} = new_worker(worker_sup, from_pid) 90 | true = :ets.insert(monitors, {worker, ref}) 91 | {:reply, worker, %{state | overflow: overflow+1}} 92 | 93 | [] when block == true -> 94 | ref = Process.monitor(from_pid) 95 | waiting = :queue.in({from, ref}, waiting) 96 | {:noreply, %{state | waiting: waiting}, :infinity} 97 | 98 | [] -> 99 | {:reply, :full, state}; 100 | end 101 | end 102 | 103 | def handle_call(:status, _from, %{workers: workers, monitors: monitors} = state) do 104 | {:reply, {state_name(state), length(workers), :ets.info(monitors, :size)}, state} 105 | end 106 | 107 | def handle_call(_msg, _from, state) do 108 | {:reply, {:error, :invalid_message}, :ok, state} 109 | end 110 | 111 | def handle_cast({:checkin, worker}, %{monitors: monitors} = state) do 112 | case :ets.lookup(monitors, worker) do 113 | [{pid, ref}] -> 114 | true = Process.demonitor(ref) 115 | true = :ets.delete(monitors, pid) 116 | new_state = handle_checkin(pid, state) 117 | {:noreply, new_state} 118 | 119 | [] -> 120 | {:noreply, state} 121 | end 122 | end 123 | 124 | def handle_info(:start_worker_supervisor, state = %{pool_sup: pool_sup, name: name, mfa: mfa, size: size}) do 125 | {:ok, worker_sup} = Supervisor.start_child(pool_sup, supervisor_spec(name, mfa)) 126 | workers = prepopulate(size, worker_sup) 127 | {:noreply, %{state | worker_sup: worker_sup, workers: workers}} 128 | end 129 | 130 | def handle_info({:DOWN, ref, _, _, _}, state = %{monitors: monitors, workers: workers}) do 131 | case :ets.match(monitors, {:"$1", ref}) do 132 | [[pid]] -> 133 | true = :ets.delete(monitors, pid) 134 | new_state = %{state | workers: [pid|workers]} 135 | {:noreply, new_state} 136 | 137 | [[]] -> 138 | {:noreply, state} 139 | end 140 | end 141 | 142 | def handle_info({:EXIT, worker_sup, reason}, state = %{worker_sup: worker_sup}) do 143 | {:stop, reason, state} 144 | end 145 | 146 | def handle_info({:EXIT, pid, _reason}, state = %{monitors: monitors, workers: workers, worker_sup: worker_sup}) do 147 | case :ets.lookup(monitors, pid) do 148 | [{pid, ref}] -> 149 | true = Process.demonitor(ref) 150 | true = :ets.delete(monitors, pid) 151 | new_state = handle_worker_exit(pid, state) 152 | {:noreply, new_state} 153 | 154 | [] -> 155 | # NOTE: Worker crashed, no monitor 156 | case Enum.member?(workers, pid) do 157 | true -> 158 | remaining_workers = workers |> Enum.reject(fn(p) -> p == pid end) 159 | new_state = %{state | workers: [new_worker(worker_sup)|remaining_workers]} 160 | {:noreply, new_state} 161 | 162 | false -> 163 | {:noreply, state} 164 | end 165 | end 166 | end 167 | 168 | def handle_info(_info, state) do 169 | {:noreply, state} 170 | end 171 | 172 | def terminate(_reason, _state) do 173 | :ok 174 | end 175 | 176 | ##################### 177 | # Private Functions # 178 | ##################### 179 | 180 | defp name(pool_name) do 181 | :"#{pool_name}Server" 182 | end 183 | 184 | defp prepopulate(size, sup) do 185 | prepopulate(size, sup, []) 186 | end 187 | 188 | defp prepopulate(size, _sup, workers) when size < 1 do 189 | workers 190 | end 191 | 192 | defp prepopulate(size, sup, workers) do 193 | prepopulate(size-1, sup, [new_worker(sup)|workers]) 194 | end 195 | 196 | defp new_worker(sup) do 197 | {:ok, worker} = Supervisor.start_child(sup, [[]]) 198 | true = Process.link(worker) 199 | worker 200 | end 201 | 202 | # NOTE: We use this when we have to queue up the consumer 203 | defp new_worker(sup, from_pid) do 204 | pid = new_worker(sup) 205 | ref = Process.monitor(from_pid) 206 | {pid, ref} 207 | end 208 | 209 | defp dismiss_worker(sup, pid) do 210 | true = Process.unlink(pid) 211 | Supervisor.terminate_child(sup, pid) 212 | end 213 | 214 | def handle_checkin(pid, state) do 215 | %{worker_sup: worker_sup, 216 | workers: workers, 217 | monitors: monitors, 218 | waiting: waiting, 219 | overflow: overflow} = state 220 | 221 | case :queue.out(waiting) do 222 | {{:value, {from, ref}}, left} -> 223 | true = :ets.insert(monitors, {pid, ref}) 224 | GenServer.reply(from, pid) 225 | %{state | waiting: left} 226 | 227 | # what case? this seems to handle an inconsistent state 228 | {:empty, empty} when overflow > 0 -> 229 | :ok = dismiss_worker(worker_sup, pid) 230 | %{state | waiting: empty, overflow: overflow-1} 231 | 232 | {:empty, empty} -> 233 | # NOTE: This is how we used to add workers back 234 | %{state | waiting: empty, workers: [pid|workers], overflow: 0} 235 | end 236 | end 237 | 238 | defp handle_worker_exit(pid, state) do 239 | %{worker_sup: worker_sup, 240 | workers: workers, 241 | monitors: monitors, 242 | waiting: waiting, 243 | overflow: overflow} = state 244 | 245 | case :queue.out(waiting) do 246 | {{:value, {from, ref}}, left} -> 247 | new_worker = new_worker(worker_sup) 248 | true = :ets.insert(monitors, {new_worker, ref}) 249 | GenServer.reply(from, new_worker) 250 | %{state | waiting: left} 251 | 252 | # NOTE: Dynamically create worker has exited. We don't 253 | # have to re-create it. 254 | {:empty, empty} when overflow > 0 -> 255 | %{state | overflow: overflow-1, waiting: empty} 256 | 257 | # NOTE: We need to create a another worker because this 258 | # worker was _not_ dynamically created. 259 | {:empty, empty} -> 260 | workers = [new_worker(worker_sup) | workers |> Enum.reject(fn(p) -> p != pid end)] 261 | %{state | workers: workers, waiting: empty} 262 | end 263 | end 264 | 265 | defp supervisor_spec(name, mfa) do 266 | # NOTE: The reason this is set to temporary is because the WorkerSupervisor 267 | # is started by the PoolServer. 268 | opts = [id: name <> "WorkerSupervisor", shutdown: 10000, restart: :temporary] 269 | supervisor(Pooly.WorkerSupervisor, [self(), mfa], opts) 270 | end 271 | 272 | defp state_name(%State{overflow: overflow, max_overflow: max_overflow, workers: workers}) when overflow < 1 do 273 | case length(workers) == 0 do 274 | true -> 275 | if max_overflow < 1 do 276 | :full 277 | else 278 | :overflow 279 | end 280 | false -> 281 | :ready 282 | end 283 | end 284 | 285 | defp state_name(%State{overflow: max_overflow, max_overflow: max_overflow}) do 286 | :full 287 | end 288 | 289 | defp state_name(_state) do 290 | :overflow 291 | end 292 | 293 | end 294 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-4/lib/pooly/pool_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Pooly.PoolSupervisor do 2 | use Supervisor 3 | 4 | def start_link(pool_config) do 5 | # NOTE: :name must be an atom! 6 | Supervisor.start_link(__MODULE__, pool_config, [name: :"#{pool_config[:name]}Supervisor"]) 7 | end 8 | 9 | def init(pool_config) do 10 | opts = [ 11 | strategy: :one_for_all 12 | ] 13 | 14 | children = [ 15 | worker(Pooly.PoolServer, [self(), pool_config]) 16 | ] 17 | 18 | supervise(children, opts) 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-4/lib/pooly/pools_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Pooly.PoolsSupervisor do 2 | use Supervisor 3 | 4 | def start_link do 5 | Supervisor.start_link(__MODULE__, [], name: __MODULE__) 6 | end 7 | 8 | def init(_) do 9 | opts = [ 10 | strategy: :one_for_one 11 | ] 12 | 13 | supervise([], opts) 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-4/lib/pooly/sample_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule SampleWorker do 2 | use GenServer 3 | 4 | def start_link(_) do 5 | GenServer.start_link(__MODULE__, :ok, []) 6 | end 7 | 8 | def stop(pid) do 9 | GenServer.call(pid, :stop) 10 | end 11 | 12 | def work_for(pid, duration) do 13 | GenServer.cast(pid, {:work_for, duration}) 14 | end 15 | 16 | def handle_call(:stop, _from, state) do 17 | {:stop, :normal, :ok, state} 18 | end 19 | 20 | def handle_cast({:work_for, duration}, state) do 21 | :timer.sleep(duration) 22 | {:stop, :normal, state} 23 | end 24 | 25 | end 26 | 27 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-4/lib/pooly/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Pooly.Server do 2 | use GenServer 3 | import Supervisor.Spec 4 | 5 | ####### 6 | # API # 7 | ####### 8 | 9 | def start_link(pools_config) do 10 | GenServer.start_link(__MODULE__, pools_config, name: __MODULE__) 11 | end 12 | 13 | def checkout(pool_name, block, timeout) do 14 | Pooly.PoolServer.checkout(pool_name, block, timeout) 15 | end 16 | 17 | def checkin(pool_name, worker_pid) do 18 | Pooly.PoolServer.checkin(pool_name, worker_pid) 19 | end 20 | 21 | def transaction(pool_name, fun, timeout) do 22 | worker = checkout(pool_name, true, timeout) 23 | try do 24 | fun.(worker) 25 | after 26 | checkin(pool_name, worker) 27 | end 28 | end 29 | 30 | def status(pool_name) do 31 | Pooly.PoolServer.status(pool_name) 32 | end 33 | 34 | ############# 35 | # Callbacks # 36 | ############# 37 | 38 | def init(pools_config) do 39 | pools_config |> Enum.each(fn(pool_config) -> 40 | send(self(), {:start_pool, pool_config}) 41 | end) 42 | 43 | {:ok, pools_config} 44 | end 45 | 46 | def handle_info({:start_pool, pool_config}, state) do 47 | {:ok, _pool_sup} = Supervisor.start_child(Pooly.PoolsSupervisor, supervisor_spec(pool_config)) 48 | 49 | {:noreply, state} 50 | end 51 | 52 | ##################### 53 | # Private Functions # 54 | ##################### 55 | 56 | defp supervisor_spec(pool_config) do 57 | # TODO: WHAT SHOULD BE GOOD VALUES 58 | # NOTE: This needs to be random because by de 59 | opts = [id: :"#{pool_config[:name]}Supervisor"] 60 | supervisor(Pooly.PoolSupervisor, [pool_config], opts) 61 | end 62 | 63 | end 64 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-4/lib/pooly/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Pooly.Supervisor do 2 | use Supervisor 3 | 4 | def start_link(pools_config) do 5 | Supervisor.start_link(__MODULE__, pools_config, name: __MODULE__) 6 | end 7 | 8 | def init(pools_config) do 9 | children = [ 10 | supervisor(Pooly.PoolsSupervisor, []), 11 | worker(Pooly.Server, [pools_config]) 12 | ] 13 | 14 | opts = [strategy: :one_for_all, 15 | max_restart: 1, 16 | max_time: 3600] 17 | 18 | supervise(children, opts) 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-4/lib/pooly/worker_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Pooly.WorkerSupervisor do 2 | use Supervisor 3 | 4 | def start_link(pool_server, {_,_,_} = mfa) do 5 | Supervisor.start_link(__MODULE__, [pool_server, mfa]) 6 | end 7 | 8 | def init([pool_server, {m,f,a}]) do 9 | Process.link(pool_server) 10 | # NOTE: Restart temporary means that we don't let the 11 | # supervisor restart the worker. Instead, the 12 | # PoolServer handle it instead. 13 | worker_opts = [restart: :temporary, 14 | shutdown: 5000, 15 | function: f] 16 | 17 | children = [worker(m, a, worker_opts)] 18 | opts = [strategy: :simple_one_for_one, 19 | max_restart: 5, 20 | max_time: 3600] 21 | 22 | supervise(children, opts) 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-4/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Pooly.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :pooly, 6 | version: "0.0.1", 7 | elixir: "~> 1.0", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps()] 11 | end 12 | 13 | def application do 14 | [applications: [:logger], 15 | mod: {Pooly, []}] # hardcoded first. 16 | end 17 | 18 | defp deps do 19 | [] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-4/test/pooly_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PoolyTest do 2 | use ExUnit.Case 3 | 4 | test "the truth" do 5 | assert 1 + 1 == 2 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /chapter_7/pooly/version-4/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /chapter_8/blitzy/README.md: -------------------------------------------------------------------------------- 1 | Blitzy - A simple HTTP load tester in Elixir 2 | ============================================ 3 | 4 | ![](http://i.imgur.com/Z8zyXZu.gif) 5 | 6 | Inspired by this [post](http://www.watchsumo.com/posts/introduction-to-elixir-v1-0-0-by-example-i) by Victor Martinez of WatchSumo. 7 | 8 | ``` 9 | % ./blitzy -n 100 http://www.bieberfever.com 10 | ``` 11 | 12 | ## Distributed Blitzy 13 | 14 | It is _way_ more fun to start distributed. Edit the provided `config/config.exs` with whatever node name suits your fancy. This is optional, and you can stick to the provided one. 15 | 16 | ```elixir 17 | config :blitz, master_node: :"a@127.0.0.1" 18 | 19 | config :blitz, slave_nodes: [:"b@127.0.0.1", 20 | :"c@127.0.0.1", 21 | :"d@127.0.0.1"] 22 | ``` 23 | 24 | Here, the master node is `:a@127.0.0.1`; the rest are slave nodes. 25 | 26 | Start up a couple of nodes, and name them accordingly. For example, here's how to start one of them: 27 | 28 | ``` 29 | % iex --name b@127.0.0.1 -S mix 30 | ``` 31 | 32 | Now, when you run the the command 33 | 34 | ``` 35 | % ./blitzy -n 100 http://www.bieberfever.com 36 | ``` 37 | 38 | the requests will be split across the number of nodes you created, including the master node. Here's an example run: 39 | 40 | ``` 41 | 17:03:30.600 [info] worker [a@127.0.0.1-256] completed in 5451.854 msecs 42 | 43 | 17:03:30.600 [info] worker [b@127.0.0.1-289] completed in 5258.639999999999 msecs 44 | 45 | 17:03:30.600 [info] worker [b@127.0.0.1-278] completed in 5272.281 msecs 46 | 47 | 17:03:30.600 [info] worker [a@127.0.0.1-310] completed in 5452.012 msecs 48 | 49 | 17:03:30.600 [info] worker [b@127.0.0.1-290] completed in 5258.318 msecs 50 | 51 | 17:03:30.600 [info] worker [b@127.0.0.1-237] completed in 5300.413 msecs 52 | ... 53 | 17:03:31.023 [info] worker [a@127.0.0.1-322] completed in 5653.303 msecs 54 | Succeeded : 50 55 | Failures : 0 56 | Total time (msecs): 542665.9879999999 57 | Avg time (msecs): 1629.6275915915912 58 | 59 | 60 | 17:03:31.024 [info] worker [c@127.0.0.1-22] completed in 5609.749 msecs 61 | Succeeded : 50 62 | Failures : 0 63 | Total time (msecs): 485414.8010000001 64 | Avg time (msecs): 1457.7021051051054 65 | ``` 66 | 67 | ## Building the Executable 68 | 69 | ``` 70 | mix escript.build 71 | ``` 72 | 73 | 74 | -------------------------------------------------------------------------------- /chapter_8/blitzy/blitzy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjamintanweihao/the-little-elixir-otp-guidebook-code/78b372fec5bc6f93203707c72a18dabe0fbaca3d/chapter_8/blitzy/blitzy -------------------------------------------------------------------------------- /chapter_8/blitzy/config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :blitzy, master_node: :"a@127.0.0.1" 4 | 5 | config :blitzy, slave_nodes: [:"b@127.0.0.1", 6 | :"c@127.0.0.1", 7 | :"d@127.0.0.1"] 8 | 9 | 10 | -------------------------------------------------------------------------------- /chapter_8/blitzy/lib/blitzy.ex: -------------------------------------------------------------------------------- 1 | defmodule Blitzy do 2 | use Application 3 | 4 | def start(_type, _args) do 5 | Blitzy.Supervisor.start_link(:ok) 6 | end 7 | 8 | end 9 | -------------------------------------------------------------------------------- /chapter_8/blitzy/lib/cli.ex: -------------------------------------------------------------------------------- 1 | defmodule Blitzy.CLI do 2 | alias Blitzy.TasksSupervisor 3 | require Logger 4 | 5 | def main(args) do 6 | Application.get_env(:blitzy, :master_node) 7 | |> Node.start 8 | 9 | Application.get_env(:blitzy, :slave_nodes) 10 | |> Enum.each(&Node.connect(&1)) 11 | 12 | args 13 | |> parse_args 14 | |> process_options([node|Node.list]) 15 | end 16 | 17 | defp parse_args(args) do 18 | OptionParser.parse(args, aliases: [n: :requests], 19 | strict: [requests: :integer]) 20 | end 21 | 22 | defp process_options(options, nodes) do 23 | case options do 24 | {[requests: n], [url], []} -> 25 | do_requests(n, url, nodes) 26 | 27 | _ -> 28 | do_help 29 | 30 | end 31 | end 32 | 33 | defp do_requests(n_requests, url, nodes) do 34 | Logger.info "Pummelling #{url} with #{n_requests} requests" 35 | 36 | total_nodes = Enum.count(nodes) 37 | req_per_node = div(n_requests, total_nodes) 38 | 39 | nodes 40 | |> Enum.flat_map(fn node -> 41 | 1..req_per_node |> Enum.map(fn _ -> 42 | Task.Supervisor.async({TasksSupervisor, node}, Blitzy.Worker, :start, [url]) 43 | end) 44 | end) 45 | |> Enum.map(&Task.await(&1, :infinity)) 46 | |> parse_results 47 | end 48 | 49 | defp do_help do 50 | IO.puts """ 51 | Usage: 52 | blitzy -n [requests] [url] 53 | 54 | Options: 55 | -n, [--requests] # Number of requests 56 | 57 | Example: 58 | ./blitzy -n 100 http://www.bieberfever.com 59 | """ 60 | System.halt(0) 61 | end 62 | 63 | defp parse_results(results) do 64 | {successes, _failures} = 65 | results 66 | |> Enum.partition(fn x -> 67 | case x do 68 | {:ok, _} -> true 69 | _ -> false 70 | end 71 | end) 72 | 73 | total_workers = Enum.count(results) 74 | total_success = Enum.count(successes) 75 | total_failure = total_workers - total_success 76 | 77 | data = successes |> Enum.map(fn {:ok, time} -> time end) 78 | average_time = average(data) 79 | longest_time = Enum.max(data) 80 | shortest_time = Enum.min(data) 81 | 82 | IO.puts """ 83 | Total workers : #{total_workers} 84 | Successful reqs : #{total_success} 85 | Failed reqs : #{total_failure} 86 | Average (msecs) : #{average_time} 87 | Longest (msecs) : #{longest_time} 88 | Shortest (msecs) : #{shortest_time} 89 | """ 90 | end 91 | 92 | defp average(list) do 93 | sum = Enum.sum(list) 94 | if sum > 0 do 95 | sum / Enum.count(list) 96 | else 97 | 0 98 | end 99 | end 100 | 101 | end 102 | -------------------------------------------------------------------------------- /chapter_8/blitzy/lib/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Blitzy.Supervisor do 2 | use Supervisor 3 | 4 | def start_link(:ok) do 5 | Supervisor.start_link(__MODULE__, :ok) 6 | end 7 | 8 | def init(:ok) do 9 | children = [ 10 | supervisor(Task.Supervisor, [[name: Blitzy.TasksSupervisor]]) 11 | ] 12 | 13 | supervise(children, [strategy: :one_for_one]) 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /chapter_8/blitzy/lib/worker.ex: -------------------------------------------------------------------------------- 1 | defmodule Blitzy.Worker do 2 | use Timex 3 | require Logger 4 | 5 | def start(url) do 6 | {timestamp, response} = Duration.measure(fn -> HTTPoison.get(url) end) 7 | handle_response({Duration.to_milliseconds(timestamp), response}) 8 | end 9 | 10 | defp handle_response({msecs, {:ok, %HTTPoison.Response{status_code: code}}}) 11 | when code >= 200 and code <= 304 do 12 | Logger.info "worker [#{node}-#{inspect self}] completed in #{msecs} msecs" 13 | {:ok, msecs} 14 | end 15 | 16 | defp handle_response({_msecs, {:error, reason}}) do 17 | Logger.info "worker [#{node}-#{inspect self}] error due to #{inspect reason}" 18 | {:error, reason} 19 | end 20 | 21 | defp handle_response({_msecs, _}) do 22 | Logger.info "worker [#{node}-#{inspect self}] errored out" 23 | {:error, :unknown} 24 | end 25 | 26 | end 27 | -------------------------------------------------------------------------------- /chapter_8/blitzy/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Blitzy.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :blitzy, 6 | version: "0.0.1", 7 | elixir: "~> 1.1", 8 | escript: escript, 9 | deps: deps] 10 | end 11 | 12 | def escript do 13 | [main_module: Blitzy.CLI] 14 | end 15 | 16 | def application do 17 | [mod: {Blitzy, []}, 18 | applications: [:logger, :httpoison, :timex]] 19 | end 20 | 21 | defp deps do 22 | [ 23 | {:httpoison, "~> 0.9.0"}, 24 | {:timex, "~> 3.0"}, 25 | {:tzdata, "~> 0.1.8", override: true} 26 | ] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /chapter_8/blitzy/mix.lock: -------------------------------------------------------------------------------- 1 | %{"certifi": {:hex, :certifi, "0.4.0", "a7966efb868b179023618d29a407548f70c52466bf1849b9e8ebd0e34b7ea11f", [:rebar3], []}, 2 | "combine": {:hex, :combine, "0.9.1", "5fd778ee77032ae593bf79aedb8519d9e36283e4f869abd98c2d6029ca476db8", [:mix], []}, 3 | "gettext": {:hex, :gettext, "0.11.0", "80c1dd42d270482418fa158ec5ba073d2980e3718bacad86f3d4ad71d5667679", [:mix], []}, 4 | "hackney": {:hex, :hackney, "1.6.1", "ddd22d42db2b50e6a155439c8811b8f6df61a4395de10509714ad2751c6da817", [:rebar3], [{:ssl_verify_fun, "1.1.0", [hex: :ssl_verify_fun, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:certifi, "0.4.0", [hex: :certifi, optional: false]}]}, 5 | "httpoison": {:hex, :httpoison, "0.9.0", "68187a2daddfabbe7ca8f7d75ef227f89f0e1507f7eecb67e4536b3c516faddb", [:mix], [{:hackney, "~> 1.6.0", [hex: :hackney, optional: false]}]}, 6 | "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []}, 7 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, 8 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, 9 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.0", "edee20847c42e379bf91261db474ffbe373f8acb56e9079acb6038d4e0bf414f", [:rebar, :make], []}, 10 | "ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.5", "2e73e068cd6393526f9fa6d399353d7c9477d6886ba005f323b592d389fb47be", [:make], []}, 11 | "timex": {:hex, :timex, "3.0.3", "d2c763b475ee4543054fe592ebeea0c0430a910be6f22a1cdebf790abaec3b91", [:mix], [{:gettext, "~> 0.10", [hex: :gettext, optional: false]}, {:combine, "~> 0.7", [hex: :combine, optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, optional: false]}]}, 12 | "tzdata": {:hex, :tzdata, "0.1.201605", "0c4184819b9d6adedcc02107b68321c45d8e853def7a32629b7961b9f2e95f33", [:mix], []}} 13 | -------------------------------------------------------------------------------- /chapter_8/blitzy/test/blitzy_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BlitzyTest do 2 | use ExUnit.Case 3 | 4 | test "the truth" do 5 | assert 1 + 1 == 2 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /chapter_8/blitzy/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /chapter_9/chucky/README.md: -------------------------------------------------------------------------------- 1 | # Chucky - A Distributed and Fault-Tolerant Chuck Norris Facts Disher 2 | 3 | ![Chuck](http://i.imgur.com/wwFsWiA.jpg) 4 | 5 | ## Step 1: Figure out your hostname 6 | 7 | ``` 8 | % hostname -s 9 | imac 10 | ``` 11 | 12 | ## Step 2: Configure `config/NODE_NAME.config` 13 | 14 | Here's an example: 15 | 16 | ```elixir 17 | [{kernel, 18 | [{distributed, 19 | [{chucky, 20 | 5000, 21 | ['a@imac', {'b@imac', 'c@imac'}]}]}, 22 | {sync_nodes_mandatory, ['b@imac', 'c@imac']}, 23 | {sync_nodes_timeout, 30000} 24 | ]}]. 25 | ``` 26 | 27 | ## Step 3: Compile 28 | 29 | 30 | ``` 31 | % mix compile 32 | ``` 33 | 34 | ## Step 4: Run it! 35 | 36 | Open 3 different terminals, and on each of them, run these commands: 37 | 38 | ``` 39 | % iex --sname a -pa _build/dev/lib/chucky/ebin --app chucky --erl "-config config/a.config" 40 | 41 | % iex --sname b -pa _build/dev/lib/chucky/ebin --app chucky --erl "-config config/b.config" 42 | 43 | % iex --sname c -pa _build/dev/lib/chucky/ebin --app chucky --erl "-config config/c.config" 44 | ``` 45 | 46 | In each terminal, run: 47 | 48 | ```elixir 49 | iex > Chucky.fact 50 | "In a fight between Batman and Darth Vader, the winner would be Chuck Norris." 51 | ``` 52 | 53 | You can also use `Application.started_applications/1` to see where the application is being run on. 54 | 55 | ## Step 5: Watching failover in action 56 | 57 | Kill the first session (`a@HOSTNAME`), then watch `b@HOSTNAME` get started: 58 | 59 | ``` 60 | 07:33:04.831 [info] b@manticore starting distributed 61 | 62 | 07:33:12.025 [info] Application chucky exited: :stopped 63 | 64 | 07:33:42.300 [info] b@manticore starting distributed 65 | ``` 66 | 67 | ## Step 6: Watching takeover in action 68 | 69 | Start `a@HOSTNAME` again: 70 | 71 | ``` 72 | % iex --sname a -pa _build/dev/lib/chucky/ebin --app chucky --erl "-config config/a.config" 73 | ``` 74 | 75 | Watch `a@HOSTNAME` take over `b@HOSTNAME`: 76 | 77 | ``` 78 | 07:39:49.820 [info] a@manticore is taking over b@manticore 79 | ``` 80 | 81 | -------------------------------------------------------------------------------- /chapter_9/chucky/config/a.config: -------------------------------------------------------------------------------- 1 | [{kernel, 2 | [{distributed, 3 | [{chucky, 4 | 5000, 5 | [a@manticore, {b@manticore, c@manticore}]}]}, 6 | {sync_nodes_mandatory, [b@manticore, c@manticore]}, 7 | {sync_nodes_timeout, 30000} 8 | ]}]. 9 | -------------------------------------------------------------------------------- /chapter_9/chucky/config/b.config: -------------------------------------------------------------------------------- 1 | [{kernel, 2 | [{distributed, 3 | [{chucky, 4 | 5000, 5 | [a@manticore, {b@manticore, c@manticore}]}]}, 6 | {sync_nodes_mandatory, [a@manticore, c@manticore]}, 7 | {sync_nodes_timeout, 30000} 8 | ]}]. 9 | -------------------------------------------------------------------------------- /chapter_9/chucky/config/c.config: -------------------------------------------------------------------------------- 1 | [{kernel, 2 | [{distributed, 3 | [{chucky, 4 | 5000, 5 | [a@manticore, {b@manticore, c@manticore}]}]}, 6 | {sync_nodes_mandatory, [a@manticore, b@manticore]}, 7 | {sync_nodes_timeout, 30000} 8 | ]}]. 9 | -------------------------------------------------------------------------------- /chapter_9/chucky/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :chucky, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:chucky, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /chapter_9/chucky/facts.txt: -------------------------------------------------------------------------------- 1 | Chuck Noris works out 25 hours a day. 2 | God didn't make all men equal, but Samuel Colt did... then God made Chuck Norris 3 | If you have five dollars and Chuck Norris has five dollars, Chuck Norris has more money than you. 4 | There is no 'ctrl' button on Chuck Norris's computer. Chuck Norris is always in control. 5 | Apple pays Chuck Norris 99 cents every time he listens to a song. 6 | Chuck Norris can sneeze with his eyes open. 7 | Chuck Norris can eat just one Lay's potato chip. 8 | Chuck Norris destroyed the periodic table, because he only recognizes the element of surprise. 9 | Chuck Norris can kill two stones with one bird. 10 | When Chuck Norris calls 1-900 numbers, he doesn't get charged. He holds up the phone and money falls out. 11 | Chuck Norris once ate a whole cake before his friends could tell him there was a stripper in it. 12 | Some people like to eat frogs' legs. Chuck Norris likes to eat lizard legs. Hence, snakes. 13 | There are no races, only countries of people Chuck Norris has beaten to different shades of black and blue. 14 | When Chuck Norris was denied an Egg McMuffin at McDonald's because it was 10:35, he roundhouse kicked the store so hard it became a Wendy's. 15 | Chuck Norris can't finish a "color by numbers" because his markers are filled with the blood of his victims. Unfortunately, all blood is dark red. 16 | A Chuck Norris-delivered Roundhouse Kick is the preferred method of execution in 16 states. 17 | When Chuck Norris falls in water, Chuck Norris doesn't get wet. Water gets Chuck Norris. 18 | Scientists have estimated that the energy given off during the Big Bang is roughly equal to 1CNRhK (Chuck Norris Roundhouse Kick) 19 | Chuck Norris’ house has no doors, only walls that he walks through. 20 | How much boner would a bonerchuck chuck if a bonerchuck could Chuck Norris? ...All of it. 21 | Chuck Norris doesn't actually write books, the words assemble themselves out of fear. 22 | In honor of Chuck Norris, all McDonald's in Texas have an even larger size than the super-size. When ordering, just ask to be Chucksized. 23 | Chuck Norris CAN believe it's not butter. 24 | If tapped, a Chuck Norris roundhouse kick could power the country of Australia for 44 minutes. 25 | Chuck Norris can divide by zero. 26 | The grass is always greener on the other side, unless Chuck Norris has been there. In that case the grass is most likely soaked in blood and tears. 27 | A picture is worth a thousand words. A Chuck Norris is worth 1 billion words. 28 | Newton's Third Law is wrong: Although it states that for each action, there is an equal and opposite reaction, there is no force equal in reaction to a Chuck Norris roundhouse kick. 29 | Chuck Norris invented his own type of karate. It's called Chuck-Will-Kill. 30 | When an episode of Walker Texas Ranger was aired in France, the French surrendered to Chuck Norris just to be on the safe side. 31 | While urinating, Chuck Norris is easily capable of welding titanium. 32 | Chuck Norris once sued the Houghton-Mifflin textbook company when it became apparent that their account of the war of 1812 was plagiarized from his autobiography. 33 | When Chuck Norris talks, everybody listens. And dies. 34 | When Steven Seagal kills a ninja, he only takes its hide. When Chuck Norris kills a ninja, he uses every part. 35 | Contrary to popular belief, there is indeed enough Chuck Norris to go around. 36 | Chuck Norris doesn't shave; he kicks himself in the face. The only thing that can cut Chuck Norris is Chuck Norris. 37 | For some, the left testicle is larger than the right one. For Chuck Norris, each testicle is larger than the other one. 38 | Chuck Norris always knows the EXACT location of Carmen San Diego. 39 | When taking the SAT, write "Chuck Norris" for every answer. You will score over 9000. 40 | Chuck Norris invented black. In fact, he invented the entire spectrum of visible light. Except pink. Tom Cruise invented pink. 41 | When you're Chuck Norris, anything + anything is equal to 1. One roundhouse kick to the face. 42 | Chuck Norris has the greatest Poker-Face of all time. He won the 1983 World Series of Poker, despite holding only a Joker, a Get out of Jail Free Monopoly card, a 2 of clubs, 7 of spades and a green #4 card from the game UNO. 43 | On his birthday, Chuck Norris randomly selects one lucky child to be thrown into the sun. 44 | Nobody doesn't like Sara Lee. Except Chuck Norris. 45 | Chuck Norris doesn't throw up if he drinks too much. Chuck Norris throws down! 46 | In the beginning there was nothing...then Chuck Norris Roundhouse kicked that nothing in the face and said "Get a job". That is the story of the universe. 47 | Chuck Norris has 12 moons. One of those moons is the Earth. 48 | Chuck Norris grinds his coffee with his teeth and boils the water with his own rage. 49 | Archaeologists unearthed an old English dictionary dating back to the year 1236. It defined "victim" as "one who has encountered Chuck Norris" 50 | Chuck Norris ordered a Big Mac at Burger King, and got one. 51 | Chuck Norris and Mr. T walked into a bar. The bar was instantly destroyed, as that level of awesome cannot be contained in one building. 52 | If you Google search "Chuck Norris getting his ass kicked" you will generate zero results. It just doesn't happen. 53 | Chuck Norris can drink an entire gallon of milk in thirty-seven seconds. 54 | Little known medical fact: Chuck Norris invented the Caesarean section when he roundhouse-kicked his way out of his mother's womb. 55 | Chuck Norris doesn't bowl strikes, he just knocks down one pin and the other nine faint. 56 | The show Survivor had the original premise of putting people on an island with Chuck Norris. There were no survivors, and nobody is brave enough to go to the island to retrieve the footage. 57 | It takes Chuck Norris 20 minutes to watch 60 Minutes. 58 | You know how they say if you die in your dream then you will die in real life? In actuality, if you dream of death then Chuck Norris will find you and kill you. 59 | Chuck Norris has a deep and abiding respect for human life... unless it gets in his way. 60 | The Bermuda Triangle used to be the Bermuda Square, until Chuck Norris Roundhouse kicked one of the corners off. 61 | There are no weapons of mass destruction in Iraq, Chuck Norris lives in Oklahoma. 62 | Chuck Norris doesn't believe in Germany. 63 | When Chuck Norris is in a crowded area, he doesn't walk around people. He walks through them. 64 | Chuck Norris once ate an entire bottle of sleeping pills. They made him blink. 65 | James Cameron wanted Chuck Norris to play the Terminator. However, upon reflection, he realized that would have turned his movie into a documentary, so he went with Arnold Schwarzenegger. 66 | Chuck Norris can touch MC Hammer. 67 | Thousands of years ago Chuck Norris came across a bear. It was so terrified that it fled north into the arctic. It was also so terrified that all of its descendents now have white hair. 68 | Chuck Norris played Russian Roulette with a fully loaded gun and won. 69 | It takes 14 puppeteers to make Chuck Norris smile, but only 2 to make him destroy an orphanage. 70 | Chuck Norris is responsible for China's over-population. He hosted a Karate tournament in Beijing and all women within 1,000 miles became pregnant instantly. 71 | Some people wear Superman pajamas. Superman wears Chuck Norris pajamas. 72 | Chuck Norris once worked as a weatherman for the San Diego evening news. Every night he would make the same forecast: Partly cloudy with a 75% chance of Pain. 73 | Simply by pulling on both ends, Chuck Norris can stretch diamonds back into coal. 74 | When Chuck Norris does a push-up, he isn't lifting himself up, he's pushing the Earth down. 75 | Chuck Norris invented the bolt-action rifle, liquor, sexual intercourse, and football-- in that order. 76 | A high tide means Chuck Norris is flying over your coast. The tide is caused by God pissing his pants. 77 | Chuck Norris keeps his friends close and his enemies closer. Close enough to drop them with one round house kick to the face. 78 | There is in fact an “I” in Norris, but there is no “team”… not even close. 79 | Scotty in Star Trek often says “Ye cannae change the laws of physics.” This is untrue. Chuck Norris can change the laws of physics. With his fists. 80 | Chuck Norris doesn't stub his toes. He accidentally destroys chairs, bedframes, and sidewalks. 81 | Using his trademark roundhouse kick, Chuck Norris once made a field goal in RJ Stadium in Tampa Bay from the 50 yard line of Qualcomm stadium in San Diego. 82 | Chuck Norris roundhouse kicks don't really kill people. They wipe out their entire existence from the space-time continuum. 83 | Chuck Norris does not own a stove, oven, or microwave , because revenge is a dish best served cold. 84 | Tom Clancy has to pay royalties to Chuck Norris because "The Sum of All Fears" is the name of Chuck Norris' autobiography. 85 | Chuck Norris can slam a revolving door. 86 | Chuck Norris is expected to win gold in every swimming competition at the 2008 Beijing Olympics, even though Chuck Norris does not swim. This is because when Chuck Norris enters the water, the water gets out of his way and Chuck Norris simply walks across the pool floor. 87 | Chuck Norris built a better mousetrap, but the world was too frightened to beat a path to his door. 88 | The original draft of The Lord of the Rings featured Chuck Norris instead of Frodo Baggins. It was only 5 pages long, as Chuck roundhouse-kicked Sauron’s ass halfway through the first chapter. 89 | Helen Keller's favorite color is Chuck Norris. 90 | Chuck Norris eats beef jerky and craps gunpowder. Then, he uses that gunpowder to make a bullet, which he uses to kill a cow and make more beef jerky. Some people refer to this as the "Circle of Life." 91 | If, by some incredible space-time paradox, Chuck Norris would ever fight himself, he'd win. Period. 92 | Chuck Norris is currently suing Myspace for taking the name of what he calls everything around you. 93 | The crossing lights in Chuck Norris's home town say "Die slowly" and "die quickly". They each have a picture of Chuck Norris punching or kicking a pedestrian. 94 | Science Fact: Roundhouse kicks are comprised primarily of an element called Chucktanium. 95 | The Sherman tank was originally called the Norris tank until Chuck Norris decided it wasn't tough enough to be associated with him. The Army, for fear of Chuck Norris, renamed the tank and promised to develop a weapon more fitting of his name. To date, no weapon created has been badass enough to be named after Chuck Norris. 96 | Chuck Norris proved that we are alone in the universe. We weren't before his first space expedition. 97 | Superman once watched an episode of Walker, Texas Ranger. He then cried himself to sleep. 98 | Chuck Norris doesn't step on toes. Chuck Norris steps on necks. 99 | The movie "Delta Force" was extremely hard to make because Chuck had to downplay his abilities. The first few cuts were completely unbelievable. 100 | Movie trivia: The movie "Invasion U.S.A." is, in fact, a documentary. 101 | Chuck Norris does not "style" his hair. It lays perfectly in place out of sheer terror. 102 | There is no such thing as global warming. Chuck Norris was cold, so he turned the sun up. 103 | A study showed the leading causes of death in the United States are: 1. Heart disease, 2. Chuck Norris, 3. Cancer 104 | It's widely believed that Jesus was Chuck Norris' stunt double for crucifixion due to the fact that it is impossible for nails to pierce Chuck Norris' skin. 105 | Chuck Norris did in fact, build Rome in a day. 106 | Along with his black belt, Chuck Norris often chooses to wear brown shoes. No one has DARED call him on it. Ever. 107 | Anytime someone is elected president in the United States, they must ask permission from Chuck Norris to live in the White House. The reason for this is because Chuck Norris had won every Federal, State, and Local election since 1777. He just allows others to run the country in his place. 108 | Once you go Norris, you are physically unable to go back. 109 | Ninjas want to grow up to be just like Chuck Norris. But usually they grow up just to be killed by Chuck Norris. 110 | Chuck Norris once sued Burger King after they refused to put razor wire in his Whopper Jr, insisting that that actually is "his" way. 111 | The last thing you hear before Chuck Norris gives you a roundhouse kick? No one knows because dead men tell no tales. 112 | Chuck Norris doesn't play god. Playing is for children. 113 | Chuck Norris is the only person in the world that can actually email a roundhouse kick. 114 | Chuck Norris won super bowls VII and VIII singlehandedly before unexpectedly retiring to pursue a career in ass-kicking. 115 | Wo hu cang long. The translation from Mandarin Chinese reads: "Crouching Chuck, Hidden Norris" 116 | Chuck Norris can set ants on fire with a magnifying glass. At night. 117 | Some kids play Kick the can. Chuck Norris played Kick the keg. 118 | 'Icy-Hot' is too weak for Chuck Norris. After a workout, Chuck Norris rubs his muscles down with liquid-hot MAGMA. 119 | Chuck Norris cannot love, he can only not kill. 120 | When Chuck Norris was a baby, he didn't suck his mother's breast. His mother served him whiskey, straight out of the bottle. 121 | According to Einstein's theory of relativity, Chuck Norris can actually roundhouse kick you yesterday. 122 | Chuck Norris once pulled out a single hair from his beard and skewered three men through the heart with it. 123 | In an act of great philanthropy, Chuck made a generous donation to the American Cancer Society. He donated 6,000 dead bodies for scientific research. 124 | Chuck Norris’ favourite cut of meat is the roundhouse. 125 | When J. Robert Oppenheimer said "I am become death, the destroyer Of worlds", He was not referring to the atomic bomb. He was referring to the Chuck Norris Halloween costume he was wearing. 126 | Chuck Norris recently had the idea to sell his urine as a canned beverage. We know this beverage as Red Bull. 127 | Chuck Norris invented a language that incorporates karate and roundhouse kicks. So next time Chuck Norris is kicking your ass, don’t be offended or hurt, he may be just trying to tell you he likes your hat. 128 | If at first you don't succeed, you're not Chuck Norris. 129 | If Chuck Norris were a calendar, every month would be named Chucktober, and every day he'd kick your ass. 130 | Fear is not the only emotion Chuck Norris can smell. He can also detect hope, as in "I hope I don't get a roundhouse kick from Chuck Norris." 131 | Chuck Norris's show is called Walker: Texas Ranger, because Chuck Norris doesn't run. 132 | MacGyver can build an airplane out of gum and paper clips, but Chuck Norris can roundhouse-kick his head through a wall and take it. 133 | Behind every successful man, there is a woman. Behind every dead man, there is Chuck Norris. 134 | What’s known as the UFC, or Ultimate Fighting Championship, doesn’t use its full name, which happens to be “Ultimate Fighting Championship, Non-Chuck-Norris-Division”. 135 | Chuck Norris brushes his teeth with a mixture of iron shavings, industrial paint remover, and boner-grain alcohol. 136 | The easiest way to determine Chuck Norris' age is to cut him in half and count the rings. 137 | There is endless debate about the existence of the human soul. Well it does exist, and Chuck Norris finds it delicious. 138 | Most boots are made for walkin'. Chuck Norris' boots ain't that merciful. 139 | The US did not boycott the 1980 Summer Olympics in Moscow due to political reasons: Chuck Norris killed the entire US team with a single round-house kick during TaeKwonDo practice. 140 | Chuck Norris wears a live rattlesnake as a condom. 141 | The Bible was originally titled "Chuck Norris and Friends" 142 | Chuck Norris began selling the Total Gym as an ill-fated attempt to make his day-to-day opponents less laughably pathetic. 143 | Do you know why Baskin Robbins only has 31 flavors? Because Chuck Norris doesn't like Fudge Ripple. 144 | When Chuck Norris says "More cowbell", he MEANS it. 145 | On the set of Walker Texas Ranger Chuck Norris brought a dying lamb back to life by nuzzling it with his beard. As the onlookers gathered, the lamb sprang to life. Chuck Norris then roundhouse kicked it, killing it instantly. This was just to prove that the good Chuck giveth, and the good Chuck, he taketh away. 146 | Chuck Norris was what Willis was talkin' about. 147 | Google won't search for Chuck Norris because it knows you don't find Chuck Norris, he finds you. 148 | Chuck Norris can lead a horse to water AND make it drink. 149 | Nagasaki never had a bomb dropped on it. Chuck Norris jumped out of a plane and roundhouse kicked against the ground. 150 | It is scientifically impossible for Chuck Norris to have had a mortal father. The most popular theory is that he went back in time and fathered himself. 151 | Chuck Norris destroyed the periodic table, because Chuck Norris only recognizes the element of surprise. 152 | It is believed dinosaurs are extinct due to a giant meteor. That's true if you want to call Chuck Norris a giant meteor. 153 | Chuck Norris shot the sheriff, but he round house kicked the deputy. 154 | That's not Chuck Norris doing push-ups -- that's Chuck Norris moving the Earth away from the path of a deadly asteroid. 155 | Chuck Norris can judge a book by its cover. 156 | Nothing can escape the gravity of a black hole, except for Chuck Norris. Chuck Norris eats black holes. They taste like chicken. 157 | Chuck Norris does not play the lottery. It doesn't have nearly enough balls. 158 | Q: How many Chuck Norris' does it take to change a light bulb? A: None, Chuck Norris prefers to kill in the dark. 159 | As President Roosevelt said: "We have nothing to fear but fear itself. And Chuck Norris." 160 | Chuck Norris just says "no" to drugs. If he said "yes", it would collapse Colombia's infrastructure. 161 | Since 1940, the year Chuck Norris was born, roundhouse-kick related deaths have increased 13,000 percent. 162 | Crime does not pay - unless you are an undertaker following Walker, Texas Ranger, on a routine patrol. 163 | Chuck Norris does not own a house. He walks into random houses and people move. 164 | It is better to give than to receive. This is especially true of a Chuck Norris roundhouse kick. 165 | Chuck Norris is the only person to ever win a staring contest against Ray Charles and Stevie Wonder at the same time. 166 | Industrial logging isn't the cause of deforestation. Chuck Norris needs toothpicks. 167 | Chuck Norris smells what the Rock is cooking... because the Rock is Chuck Norris' personal chef. 168 | When Chuck Norris plays Oregon Trail, his family does not die from cholera or dysentery, but rather, roundhouse kicks to the face. He also requires no wagon, since he carries the oxen, axles, and buffalo meat on his back. He always makes it to Oregon before you. 169 | Chuck Norris is the reason why Waldo is hiding. 170 | "Brokeback Mountain" is not just a movie. It's also what Chuck Norris calls the pile of dead ninjas in his front yard. 171 | Chuck Norris does not eat. Food understands that the only safe haven from Chuck Norris' fists is inside his own body. 172 | Chuck Norris built a time machine and went back in time to stop the JFK assassination. As Oswald shot, Chuck met all three bullets with his beard, deflecting them. JFK's head exploded out of sheer amazement. 173 | Chuck Norris doesn't read books. He stares them down until he gets the information he wants. 174 | Chuck Norris uses a night light. Not because Chuck Norris is afraid of the dark, but the dark is afraid of Chuck Norris. 175 | Chuck Norris is not capable of hitting a target on the broad side of a barn. Every time he tries, the whole damn barn falls down. 176 | Before each filming of Walker: Texas Ranger, Chuck Norris is injected with fourteen times the lethal dose of elephant tranquilizer. This is, of course, to limit his strength and mobility, in an attempt to lower the fatality rate of the actors he fights. 177 | When Bruce Banner gets mad, he turns into the Hulk. When the Hulk gets mad, he turns into Chuck Norris. 178 | Chuck Norris kills anyone that asks, "You want fries with that" because by now everyone should know that Chuck doesn't ever want fries with anything. Ever. 179 | Chuck Norris once kicked a horse in the chin. Its descendants are known today as Giraffes. 180 | Sticks and stones may break your bones, but a Chuck Norris glare will liquefy your kidneys. 181 | Human cloning is outlawed because if Chuck Norris were cloned, then it would be possible for a Chuck Norris roundhouse kick to meet another chuck Norris roundhouse kick. Physicists theorize that this contact would end the universe. 182 | Chuck Norris once went skydiving, but promised never to do it again. One Grand Canyon is enough. 183 | Chuck Norris's version of a "chocolate milkshake" is a raw porterhouse wrapped around ten Hershey bars, and doused in diesel fuel. 184 | If Chuck Norris round-house kicks you, you will die. If Chuck Norris' misses you with the round-house kick, the wind behind the kick will tear out your pancreas. 185 | In a fight between Batman and Darth Vader, the winner would be Chuck Norris. 186 | Chuck Norris puts his pants on one leg at a time, just like the rest of us. The only difference is, then he kills people. 187 | Everybody loves Raymond. Except Chuck Norris. 188 | Contrary to popular belief, the Titanic didn't hit an iceberg. The ship was off course and accidentally ran into Chuck Norris while he was doing the backstroke across the Atlantic. 189 | Chuck Norris got his drivers license at the age of 16. Seconds. 190 | The original title for Alien vs. Predator was Alien and Predator vs Chuck Norris. The film was cancelled shortly after going into preproduction. No one would pay nine dollars to see a movie fourteen seconds long. 191 | Chuck Norris can win at solitaire with only 18 cards. 192 | Chuck Norris once shat blood - the blood of 11,940 natives he had killed and eaten. 193 | Maslow's theory of higher needs does not apply to Chuck Norris. He only has two needs: killing people and finding people to kill. 194 | The truth will set you free. Unless Chuck Norris has you, in which case, forget it buddy! 195 | For most people, home is where the heart is. For Chuck Norris, home is where he stores his collection of human skulls. 196 | Kryptonite has been found to contain trace elements of Chuck Norris roundhouse kicks to the face. This is why it is so deadly to Superman. 197 | Saddam Hussein was not found hiding in a "hole." Saddam was roundhouse-kicked in the head by Chuck Norris in Kansas, which sent him through the earth, stopping just short of the surface of Iraq. 198 | Coroners refer to dead people as "ABC's". Already Been Chucked. 199 | Chuck Norris doesn't look both ways before he crosses the street... he just roundhouses any cars that get too close. 200 | Chuck Norris does not have to answer the phone. His beard picks up the incoming electrical impulses and translates them into audible sound. 201 | How many roundhouse kicks does it take to get to the center of a tootsie pop? Just one. From Chuck Norris. 202 | Chuck Norris doesn't wear a watch, HE decides what time it is. 203 | The phrase 'break a leg' was originally coined by Chuck Norris's co-stars in Walker, Texas Ranger as a good luck charm, indicating that a broken leg might be the worst extent of their injuries. This never proved to be the case. 204 | When chuck Norris does division, there are no remainders. 205 | If you rearrange the letters in "Chuck Norris", they also spell "Crush Rock In". The words "with his fists" are understood. 206 | Never look a gift Chuck Norris in the mouth, because he will bite your damn eyes off. 207 | Give a man a fish, and you will feed him for a day. Give a man anything that is better than a fish, and Chuck Norris will beat his ass and take it. 208 | Chuck Norris used to play baseball. When Babe Ruth was hailed as the better player, Chuck Norris killed him with a baseball bat to the throat. Lou Gehrig got off easy. 209 | The original title for Star Wars was "Skywalker: Texas Ranger". Starring Chuck Norris. 210 | Guantanamo Bay, Cuba, is the military code-word for "Chuck Norris' basement". 211 | The phrase 'balls to the wall' was originally conceived to describe Chuck Norris entering any building smaller than an aircraft hangar. 212 | Chuck Norris’ roundhouse kick is so powerful, it can be seen from outer space by the naked eye. 213 | Ozzy Osbourne bites the heads off of bats. Chuck Norris bites the heads off of Siberian Tigers. 214 | He who lives by the sword, dies by the sword. He who lives by Chuck Norris, dies by the roundhouse kick. 215 | The best-laid plans of mice and men often go awry. Even the worst-laid plans of Chuck Norris come off without a hitch. 216 | The phrase 'dead ringer' refers to someone who sits behind Chuck Norris in a movie theater and forgets to turn their cell phone off. 217 | Chuck Norris' Roundhouse kick is so powerful, that on the set of Sidekicks he single-footedly destroyed Jonathan Brandis' Career. 218 | Staring at Chuck Norris for extended periods of time without proper eye protection will cause blindness, and possibly foot sized bruises on the face. 219 | Chuck Norris can taste lies. 220 | Chuck Norris does not kick ass and take names. In fact, Chuck Norris kicks ass and assigns the corpse a number. It is currently recorded to be in the billions. 221 | One time, Chuck Norris accidentally stubbed his toe. It destroyed the entire state of Ohio. 222 | Little Miss Muffet sat on her tuffet, until Chuck Norris roundhouse kicked her into a glacier. 223 | In 1990, Chuck Norris founded the non-profit organization "Kick Drugs Out of America". If the organization's name were "Roundhouse Kick Drugs out of America", there wouldn't be any drugs in the Western Hemisphere. Anywhere. 224 | Chuck Norris can blow bubbles with beef jerky. 225 | They had to edit the first ending of 'Lone Wolf McQuade' after Chuck Norris kicked David Carradine's ass, then proceeded to barbecue and eat him. 226 | Chuck Norris does, in fact, live in a round house. 227 | Chuck Norris was once on Jeopardy. This show is notable in that it was the first occasion in Jeopardy history that Alex Trebek had appeared without a mustache. And a head. 228 | 4 out of 5 doctors fail to recommend Chuck Norris as a solution to most problems. Also, 80% of doctors die unexplained, needlessly brutal deaths. 229 | Chuck Norris can skeletize a cow in two minutes. 230 | The only sure things are Death and Taxes…and when Chuck Norris goes to work for the IRS, they'll be the same thing. 231 | Chuck Norris' first job was as a paperboy. There were no survivors. 232 | With the rising cost of gasoline, Chuck Norris is beginning to worry about his drinking habit. 233 | The square root of Chuck Norris is pain. Do not try to square Chuck Norris, the result is death. 234 | chuck Norris' testicles do not produce sperm. They produce tiny white ninjas that recognize only one mission: seek and destroy. 235 | To be or not to be? That is the question. The answer? Chuck Norris. 236 | Chuck Norris has never been in a fight, ever. Do you call one roundhouse kick to the face a fight? 237 | There are two types of people in the world... people that suck, and Chuck Norris. 238 | Chuck Norris never wet his bed as a child. The bed wet itself out of fear. 239 | If you were somehow able to land a punch on Chuck Norris your entire arm would shatter upon impact. This is only in theory, since, come on, who in their right mind would try this? 240 | 70% of a human's weight is water. 70% of Chuck Norris' weight is his dick. 241 | Jean-Claude Van Damme once kicked Chuck Norris' ass. He was then awakened from his dream by a roundhouse kick to the face. 242 | The pie scene in "American Pie" is based on a dare Chuck Norris took when he was younger. However, in Chuck Norris' case, the "pie" was the molten crater of an active volcano. 243 | Chuck Norris uses 8'x10' sheets of plyboner as toilet paper. 244 | Chuck Norris was able to find and read [http://www.xkcd.com/404/ comic #404] 245 | Chuck Norris is his own operating system. He needs no security patches. He finds vulnerabilities in others. He files no bug reports. 246 | Chuck Norris doesn't need to sudo to make you to make a sandwich for him. 247 | Chuck Norris stole Randal Munroe's ability to draw. That's why most of xkcd is stick figures. 248 | Chuck Norris changed the blogocube to a blogosphere. 249 | Chuck Norris can use his fist as his SSH key. His foot is his GPG key. 250 | Don't bother trying to fix a segmentation fault. 251 | Chuck Norris won the [http://xkcd.com/381/ Mobius Battle]. 252 | Chuck Norris is an insane build environment. 253 | Chuck Norris can bend light. With his bare hands. 254 | God said "Let there be life." and Chuck Norris said "Say please." 255 | In war, Chuck Norris doesn't wear armor, the armor wears him for protection. And he always survives 256 | Chuck Norris doesn't read, he just stares the book down untill it tells him what he wants. 257 | Hey, did you know Chuck Norris has been dead for three years? Death is just afraid to tell him. 258 | Chuck Norris will live forever because, lets face it, Heaven doesn't want him, and Hell is afraid he'll take over. 259 | There is no chin under Chuck Norris's beard, only another fist 260 | What there was before the Big Bang? R: Chuck Norris 261 | Chuck Norris is the only known man that can kill himself and still survive 262 | The only person who can surf through all youtube viral video without getting Rick Roll'd is Chuck Norris 263 | Only Chuck Norris can be more invisible than the Predator. 264 | Chuck Norris doesn't wear sun-screen. The sun wears Chuck-screen. 265 | Death once had a "Near Chuck Norris Experience." 266 | Two thinks are certain in life. The fact that one day Chuck Norris will kill you and taxes. 267 | -------------------------------------------------------------------------------- /chapter_9/chucky/lib/chucky.ex: -------------------------------------------------------------------------------- 1 | defmodule Chucky do 2 | use Application 3 | require Logger 4 | 5 | def start(type, _args) do 6 | import Supervisor.Spec 7 | children = [ 8 | worker(Chucky.Server, []) 9 | ] 10 | 11 | case type do 12 | :normal -> 13 | Logger.info("Application is started on #{node}") 14 | 15 | {:takeover, old_node} -> 16 | Logger.info("#{node} is taking over #{old_node}") 17 | 18 | {:failover, old_node} -> 19 | Logger.info("#{old_node} is failing over to #{node}") 20 | end 21 | 22 | opts = [strategy: :one_for_one, name: {:global, Chucky.Supervisor}] 23 | Supervisor.start_link(children, opts) 24 | end 25 | 26 | def fact do 27 | Chucky.Server.fact 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /chapter_9/chucky/lib/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Chucky.Server do 2 | use GenServer 3 | 4 | ####### 5 | # API # 6 | ####### 7 | 8 | def start_link do 9 | GenServer.start_link(__MODULE__, [], [name: {:global, __MODULE__}]) 10 | end 11 | 12 | def fact do 13 | GenServer.call({:global, __MODULE__}, :fact) 14 | end 15 | 16 | ############# 17 | # Callbacks # 18 | ############# 19 | 20 | def init([]) do 21 | :random.seed(:os.timestamp) 22 | facts = "facts.txt" 23 | |> File.read! 24 | |> String.split("\n") 25 | 26 | {:ok, facts} 27 | end 28 | 29 | def handle_call(:fact, _from, facts) do 30 | random_fact = facts 31 | |> Enum.shuffle 32 | |> List.first 33 | 34 | {:reply, random_fact, facts} 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /chapter_9/chucky/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Chucky.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :chucky, 6 | version: "0.0.1", 7 | elixir: "~> 1.1", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps] 11 | end 12 | 13 | def application do 14 | [applications: [:logger], 15 | # registered: [Chucky, Chucky.Supervisor, Chucky.Server], 16 | mod: {Chucky, []}] 17 | end 18 | 19 | defp deps do 20 | [] 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /chapter_9/chucky/test/chucky_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ChuckyTest do 2 | use ExUnit.Case 3 | doctest Chucky 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /chapter_9/chucky/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------