├── test ├── test_helper.exs └── registry_sample │ ├── account_test.exs │ └── account_supervisor_test.exs ├── lib ├── registry_sample.ex └── registry_sample │ ├── application.ex │ ├── account_supervisor.ex │ └── account.ex ├── .gitignore ├── mix.exs ├── config └── config.exs └── README.md /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test/registry_sample/account_test.exs: -------------------------------------------------------------------------------- 1 | defmodule RegistrySample.AccountTest do 2 | use ExUnit.Case, async: true 3 | doctest RegistrySample.Account 4 | end 5 | -------------------------------------------------------------------------------- /test/registry_sample/account_supervisor_test.exs: -------------------------------------------------------------------------------- 1 | defmodule RegistrySample.AccountSupervisorTest do 2 | use ExUnit.Case, async: true 3 | doctest RegistrySample.AccountSupervisor 4 | end 5 | -------------------------------------------------------------------------------- /lib/registry_sample.ex: -------------------------------------------------------------------------------- 1 | defmodule RegistrySample do 2 | @moduledoc """ 3 | Documentation for RegistrySample. 4 | """ 5 | 6 | @doc """ 7 | Hello world. 8 | 9 | ## Examples 10 | 11 | iex> RegistrySample.hello 12 | :world 13 | 14 | """ 15 | def hello do 16 | :world 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /lib/registry_sample/application.ex: -------------------------------------------------------------------------------- 1 | defmodule RegistrySample.Application do 2 | # See http://elixir-lang.org/docs/stable/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | def start(_type, _args) do 9 | import Supervisor.Spec, warn: false 10 | 11 | # Define workers and child supervisors to be supervised 12 | children = [ 13 | supervisor(Registry, [:unique, :account_process_registry]), 14 | supervisor(RegistrySample.AccountSupervisor, []) 15 | ] 16 | 17 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 18 | # for other strategies and supported options 19 | opts = [strategy: :one_for_one, name: RegistrySample.Supervisor] 20 | Supervisor.start_link(children, opts) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule RegistrySample.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :registry_sample, 6 | version: "0.1.0", 7 | elixir: "~> 1.4", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps()] 11 | end 12 | 13 | # Configuration for the OTP application 14 | # 15 | # Type "mix help compile.app" for more information 16 | def application do 17 | # Specify extra applications you'll use from Erlang/Elixir 18 | [extra_applications: [:logger], 19 | mod: {RegistrySample.Application, []}] 20 | end 21 | 22 | # Dependencies can be Hex packages: 23 | # 24 | # {:my_dep, "~> 0.3.0"} 25 | # 26 | # Or git/path repositories: 27 | # 28 | # {:my_dep, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 29 | # 30 | # Type "mix help deps" for more examples and options 31 | defp deps do 32 | [] 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /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 :registry_sample, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:registry_sample, :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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RegistrySample 2 | 3 | Example application using the new `Registry` module in Elixir 1.4 4 | 5 | See this [blog post](https://medium.com/@adammokan/registry-in-elixir-1-4-0-d6750fb5aeb#.7cnipan20) for more context. 6 | 7 | ## The Basics 8 | 9 | ### Clone the Repo 10 | 11 | `git clone git@github.com:amokan/registry_sample.git` 12 | 13 | ### Run the sample in IEX 14 | 15 | ``` 16 | cd registry_sample 17 | iex -S mix 18 | ``` 19 | 20 | ### Scenario 21 | 22 | From the [blog post](https://medium.com/@adammokan/registry-in-elixir-1-4-0-d6750fb5aeb#.7cnipan20): 23 | 24 | _Imagine we have a scenario where we are selling widgets to customers. Our boss wants us to build a realtime UI that shows every account that has placed an order in the current day along with some basic info about each account. If an account doesn’t place another order within 24 hours, it should fall off the UI_ 25 | 26 | ### RegistrySample.AccountSupervisor 27 | 28 | The `RegistrySample.AccountSupervisor` is a `:simple_one_for_one` supervisor that gives us a few helpful functions for creating new `RegistrySample.Account` processes and indicating which processes are already running. 29 | 30 | Let's create an account process for `account_id` # 2 and `account_id` # 10. 31 | ``` 32 | iex> RegistrySample.AccountSupervisor.find_or_create_process(2) 33 | {:ok, 2} 34 | 35 | iex> RegistrySample.AccountSupervisor.find_or_create_process(10) 36 | {:ok, 2} 37 | 38 | iex> RegistrySample.AccountSupervisor.get_all_account_widgets_ordered 39 | [%{account_id: 2, widgets_sold: 1}, %{account_id: 10, widgets_sold: 1}] 40 | ``` 41 | 42 | Now we can say that `account_id` # 10 ordered another widget, by just passing the `account_id` 43 | 44 | ``` 45 | iex> RegistrySample.Account.order_widget(10) 46 | :ok 47 | 48 | iex> RegistrySample.AccountSupervisor.get_all_account_widgets_ordered 49 | [%{account_id: 2, widgets_sold: 1}, %{account_id: 10, widgets_sold: 2}] 50 | ``` 51 | 52 | -------------------------------------------------------------------------------- /lib/registry_sample/account_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule RegistrySample.AccountSupervisor do 2 | @moduledoc """ 3 | Supervisor to handle the creation of dynamic `RegistrySample.Account` processes using a 4 | `simple_one_for_one` strategy. See the `init` callback at the bottom for details on that. 5 | 6 | These processes will spawn for each `account_id` provided to the 7 | `RegistrySample.Account.start_link` function. 8 | 9 | Functions contained in this supervisor module will assist in the creation and retrieval of 10 | new account processes. 11 | 12 | Also note the guards utilizing `is_integer(account_id)` on the functions. My feeling here is that 13 | if someone makes a mistake and tries sending a string-based key or an atom, I'll just _"let it crash"_. 14 | """ 15 | 16 | use Supervisor 17 | require Logger 18 | 19 | 20 | @account_registry_name :account_process_registry 21 | 22 | @doc """ 23 | Starts the supervisor. 24 | """ 25 | def start_link, do: Supervisor.start_link(__MODULE__, [], name: __MODULE__) 26 | 27 | 28 | @doc """ 29 | Will find the process identifier (in our case, the `account_id`) if it exists in the registry and 30 | is attached to a running `RegistrySample.Account` process. 31 | 32 | If the `account_id` is not present in the registry, it will create a new `RegistrySample.Account` 33 | process and add it to the registry for the given `account_id`. 34 | 35 | Returns a tuple such as `{:ok, account_id}` or `{:error, reason}` 36 | """ 37 | def find_or_create_process(account_id) when is_integer(account_id) do 38 | if account_process_exists?(account_id) do 39 | {:ok, account_id} 40 | else 41 | account_id |> create_account_process 42 | end 43 | end 44 | 45 | 46 | @doc """ 47 | Determines if a `RegistrySample.Account` process exists, based on the `account_id` provided. 48 | 49 | Returns a boolean. 50 | 51 | ## Example 52 | iex> RegistrySample.AccountSupervisor.account_process_exists?(6) 53 | false 54 | """ 55 | def account_process_exists?(account_id) when is_integer(account_id) do 56 | case Registry.lookup(@account_registry_name, account_id) do 57 | [] -> false 58 | _ -> true 59 | end 60 | end 61 | 62 | 63 | @doc """ 64 | Creates a new account process, based on the `account_id` integer. 65 | 66 | Returns a tuple such as `{:ok, account_id}` if successful. 67 | If there is an issue, an `{:error, reason}` tuple is returned. 68 | """ 69 | def create_account_process(account_id) when is_integer(account_id) do 70 | case Supervisor.start_child(__MODULE__, [account_id]) do 71 | {:ok, _pid} -> {:ok, account_id} 72 | {:error, {:already_started, _pid}} -> {:error, :process_already_exists} 73 | other -> {:error, other} 74 | end 75 | end 76 | 77 | 78 | @doc """ 79 | Returns the count of `RegistrySample.Account` processes managed by this supervisor. 80 | 81 | ## Example 82 | iex> RegistrySample.AccountSupervisor.account_process_count 83 | 0 84 | """ 85 | def account_process_count, do: Supervisor.which_children(__MODULE__) |> length 86 | 87 | 88 | @doc """ 89 | Return a list of `account_id` integers known by the registry. 90 | 91 | ex - `[1, 23, 46]` 92 | """ 93 | def account_ids do 94 | Supervisor.which_children(__MODULE__) 95 | |> Enum.map(fn {_, account_proc_pid, _, _} -> 96 | Registry.keys(@account_registry_name, account_proc_pid) 97 | |> List.first 98 | end) 99 | |> Enum.sort 100 | end 101 | 102 | 103 | @doc """ 104 | Return a list of widgets ordered per account. 105 | 106 | The list will be made up of a map structure for each child account process. 107 | 108 | ex - `[%{account_id: 2, widgets_sold: 1}, %{account_id: 10, widgets_sold: 1}]` 109 | """ 110 | def get_all_account_widgets_ordered do 111 | account_ids() |> Enum.map(&(%{ account_id: &1, widgets_sold: RegistrySample.Account.widgets_ordered(&1) })) 112 | end 113 | 114 | 115 | @doc false 116 | def init(_) do 117 | children = [ 118 | worker(RegistrySample.Account, [], restart: :temporary) 119 | ] 120 | 121 | # strategy set to `:simple_one_for_one` to handle dynamic child processes. 122 | supervise(children, strategy: :simple_one_for_one) 123 | end 124 | 125 | end 126 | -------------------------------------------------------------------------------- /lib/registry_sample/account.ex: -------------------------------------------------------------------------------- 1 | defmodule RegistrySample.Account do 2 | @moduledoc """ 3 | Simple genserver to represent an imaginary account process. 4 | 5 | Requires you provide an integer-based `account_id` upon starting. 6 | 7 | There is a `:fetch_data` callback handler where you could easily get additional account attributes 8 | from a database or some other source - assuming the `account_id` provided was a valid key to use as 9 | database criteria. 10 | """ 11 | 12 | use GenServer 13 | require Logger 14 | 15 | @account_registry_name :account_process_registry 16 | @process_lifetime_ms 86_400_000 # 24 hours in milliseconds - make this number shorter to experiement with process termination 17 | 18 | # Just a simple struct to manage the state for this genserver 19 | # You could add additional attributes here to keep track of for a given account 20 | defstruct account_id: 0, 21 | name: "", 22 | some_attribute: "", 23 | widgets_ordered: 1, 24 | timer_ref: nil 25 | 26 | 27 | @doc """ 28 | Starts a new account process for a given `account_id`. 29 | """ 30 | def start_link(account_id) when is_integer(account_id) do 31 | GenServer.start_link(__MODULE__, [account_id], name: via_tuple(account_id)) 32 | end 33 | 34 | 35 | # registry lookup handler 36 | defp via_tuple(account_id), do: {:via, Registry, {@account_registry_name, account_id}} 37 | 38 | 39 | @doc """ 40 | Return some details (state) for this account process 41 | """ 42 | def details(account_id) do 43 | GenServer.call(via_tuple(account_id), :get_details) 44 | end 45 | 46 | 47 | @doc """ 48 | Return the number of widgets ordered by this account 49 | """ 50 | def widgets_ordered(account_id) do 51 | GenServer.call(via_tuple(account_id), :get_widgets_ordered) 52 | end 53 | 54 | 55 | @doc """ 56 | Function to indicate that this account ordered a widget 57 | """ 58 | def order_widget(account_id) do 59 | GenServer.call(via_tuple(account_id), :order_widget) 60 | end 61 | 62 | 63 | @doc """ 64 | Returns the pid for the `account_id` stored in the registry 65 | """ 66 | def whereis(account_id) do 67 | case Registry.lookup(@account_registry_name, account_id) do 68 | [{pid, _}] -> pid 69 | [] -> nil 70 | end 71 | end 72 | 73 | 74 | @doc """ 75 | Init callback 76 | """ 77 | def init([account_id]) do 78 | 79 | # Add a msg to the process mailbox to 80 | # tell this process to run `:fetch_data` 81 | send(self(), :fetch_data) 82 | send(self(), :set_terminate_timer) 83 | 84 | Logger.info("Process created... Account ID: #{account_id}") 85 | 86 | # Set initial state and return from `init` 87 | {:ok, %__MODULE__{ account_id: account_id }} 88 | end 89 | 90 | 91 | @doc """ 92 | Our imaginary callback handler to get some data from a DB to 93 | update the state on this process. 94 | """ 95 | def handle_info(:fetch_data, state = %{account_id: account_id}) do 96 | 97 | # update the state from the DB in imaginary land. Hardcoded for now. 98 | updated_state = 99 | %__MODULE__{ state | widgets_ordered: 1, name: "Account #{account_id}" } 100 | 101 | {:noreply, updated_state} 102 | end 103 | 104 | @doc """ 105 | Callback handler that sets a timer for 24 hours to terminate this process. 106 | 107 | You can call this more than once it will continue to `push out` the timer (and cleans up the previous one) 108 | 109 | I could have combined the logic below and used just one callback handler, but I like seperating the 110 | concern of creating an initial timer reference versus destroying an existing one. But that is up to you. 111 | """ 112 | def handle_info(:set_terminate_timer, %__MODULE__{ timer_ref: nil } = state) do 113 | # This is the first time we've dealt with this account, so lets set our timer reference attribute 114 | # to end this process in 24 hours from now 115 | 116 | # set a timer for 24 hours from now to end this process 117 | updated_state = 118 | %__MODULE__{ state | timer_ref: Process.send_after(self(), :end_process, @process_lifetime_ms) } 119 | 120 | {:noreply, updated_state} 121 | end 122 | def handle_info(:set_terminate_timer, %__MODULE__{ timer_ref: timer_ref } = state) do 123 | # This match indicates we are in a situation where `state.timer_ref` is not nil - 124 | # so we already have dealt with this account before 125 | 126 | # let's cancel the existing timer 127 | timer_ref |> Process.cancel_timer 128 | 129 | # set a new timer for 24 hours from now to end this process 130 | updated_state = 131 | %__MODULE__{ state | timer_ref: Process.send_after(self(), :end_process, @process_lifetime_ms) } 132 | 133 | {:noreply, updated_state} 134 | end 135 | 136 | 137 | @doc """ 138 | Gracefully end this process 139 | """ 140 | def handle_info(:end_process, state) do 141 | Logger.info("Process terminating... Account ID: #{state.account_id}") 142 | {:stop, :normal, state} 143 | end 144 | 145 | 146 | @doc false 147 | def handle_call(:get_details, _from, state) do 148 | 149 | # maybe you'd want to transform the state a bit... 150 | response = %{ 151 | id: state.account_id, 152 | name: state.name, 153 | some_attribute: state.some_attribute, 154 | widgets_ordered: state.widgets_ordered 155 | } 156 | 157 | {:reply, response, state} 158 | end 159 | 160 | 161 | @doc false 162 | def handle_call(:get_widgets_ordered, _from, %__MODULE__{ widgets_ordered: widgets_ordered } = state) do 163 | {:reply, widgets_ordered, state} 164 | end 165 | 166 | 167 | @doc false 168 | def handle_call(:order_widget, _from, %__MODULE__{ widgets_ordered: widgets_ordered } = state) do 169 | {:reply, :ok, %__MODULE__{ state | widgets_ordered: widgets_ordered + 1 }} 170 | end 171 | 172 | end 173 | --------------------------------------------------------------------------------