├── config └── config.exs ├── .formatter.exs ├── lib ├── types.ex ├── spec.ex ├── behaviour.ex └── gen_registry.ex ├── .gitignore ├── LICENSE ├── mix.exs ├── test ├── test_helper.exs ├── supervisor_test.exs └── gen_registry_test.exs ├── mix.lock ├── .github └── workflows │ └── ci.yml └── README.md /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :gen_registry, 4 | gen_module: GenServer 5 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: [ 3 | "lib/**/*.{ex,exs}", 4 | "test/**/*.{ex,exs}", 5 | "config/**/*.exs", 6 | "mix.exs" 7 | ] 8 | ] 9 | -------------------------------------------------------------------------------- /lib/types.ex: -------------------------------------------------------------------------------- 1 | defmodule GenRegistry.Types do 2 | @typedoc """ 3 | GenRegistry register a running process to an id. 4 | 5 | This id can be any valid Erlang Term, the id type is provided to clarify when an argument is 6 | intended to be an id. 7 | """ 8 | @type id :: term 9 | end 10 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /lib/spec.ex: -------------------------------------------------------------------------------- 1 | defmodule GenRegistry.Spec do 2 | @moduledoc """ 3 | GenRegistry.Spec provides helpers for pre-1.5 supervision. 4 | 5 | Starting in Elixir 1.5 the preferred way to define child specs changed from using the now 6 | deprecated `Supervisor.Spec` module to using module-based child specs. This is a legacy support 7 | module for pre-1.5 spec generation 8 | """ 9 | 10 | import Supervisor.Spec, warn: false 11 | 12 | @doc """ 13 | Returns a pre-1.5 child spec for GenRegistry 14 | """ 15 | @spec child_spec(worker_module :: module, opts :: Keyword.t()) :: Supervisor.Spec.spec() 16 | def child_spec(worker_module, opts \\ []) do 17 | opts = Keyword.put_new(opts, :name, worker_module) 18 | 19 | supervisor(GenRegistry, [worker_module, opts], id: opts[:name]) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/behaviour.ex: -------------------------------------------------------------------------------- 1 | defmodule GenRegistry.Behaviour do 2 | @moduledoc """ 3 | GenRegistry.Behaviour defines the interface that a GenRegistry module should implement. 4 | 5 | It is included to aid the development of replacement GenRegistry like implementations for 6 | special use cases or for testing. 7 | """ 8 | 9 | alias :ets, as: ETS 10 | alias GenRegistry.Types 11 | 12 | @callback count(ETS.tab()) :: integer() 13 | @callback lookup(ETS.tab(), Types.id()) :: {:ok, pid()} | {:error, :not_found} 14 | @callback lookup_or_start(GenServer.server(), Types.id(), args :: [any()], timeout :: integer()) :: 15 | {:ok, pid()} | {:error, any()} 16 | @callback reduce(ETS.tab(), any(), ({Types.id(), pid()}, any() -> any())) :: any() 17 | @callback sample(ETS.tab()) :: {Types.id(), pid()} | nil 18 | @callback start(GenServer.server(), Types.id(), args :: [any()], timeout :: integer()) :: {:ok, pid()} | {:error, {:already_started, pid()}} | {:error, any()} 19 | @callback stop(GenServer.server(), Types.id()) :: :ok | {:error, :not_found} 20 | @callback to_list(ETS.tab()) :: [{Types.id(), pid()}] 21 | end 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Discord 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule GenRegistry.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :gen_registry, 7 | version: "1.3.0", 8 | elixir: "~> 1.2", 9 | build_embedded: Mix.env() == :prod, 10 | start_permanent: Mix.env() == :prod, 11 | deps: deps(), 12 | docs: docs(), 13 | package: package() 14 | ] 15 | end 16 | 17 | def application do 18 | [ 19 | extra_applications: [:logger] 20 | ] 21 | end 22 | 23 | defp deps do 24 | [ 25 | {:ex_doc, "~> 0.28.5", only: [:dev], runtime: false}, 26 | {:dialyxir, "~> 1.0", only: [:dev], runtime: false} 27 | ] 28 | end 29 | 30 | defp docs do 31 | [ 32 | name: "GenRegistry", 33 | extras: ["README.md"], 34 | main: "readme", 35 | source_url: "https://github.com/discordapp/gen_registry" 36 | ] 37 | end 38 | 39 | defp package do 40 | [ 41 | name: :gen_registry, 42 | description: "GenRegistry provides simple management of a local registry of processes.", 43 | maintainers: ["Discord Core Infrastructure"], 44 | licenses: ["MIT"], 45 | links: %{ 46 | "GitHub" => "https://github.com/discordapp/gen_registry" 47 | } 48 | ] 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | defmodule ExampleWorker do 2 | use GenServer 3 | 4 | def start_link(value \\ nil) do 5 | GenServer.start_link(__MODULE__, value, []) 6 | end 7 | 8 | def get(pid) do 9 | GenServer.call(pid, :get) 10 | end 11 | 12 | def init(value) do 13 | case value do 14 | :invalid -> 15 | {:stop, :invalid} 16 | 17 | value -> 18 | {:ok, value} 19 | end 20 | end 21 | 22 | def handle_call(:get, _from, value) do 23 | {:reply, value, value} 24 | end 25 | end 26 | 27 | defmodule Spy do 28 | use GenServer 29 | 30 | def start_link(original) do 31 | GenServer.start_link(__MODULE__, original) 32 | end 33 | 34 | def replace(pid) when is_pid(pid) do 35 | start_link(pid) 36 | end 37 | 38 | def replace(name) do 39 | case Process.whereis(name) do 40 | nil -> 41 | {:error, :not_found} 42 | 43 | pid -> 44 | true = Process.unregister(name) 45 | {:ok, spy} = start_link(pid) 46 | Process.register(spy, name) 47 | 48 | {:ok, spy} 49 | end 50 | end 51 | 52 | def calls(pid) do 53 | GenServer.call(pid, {__MODULE__, :calls}) 54 | end 55 | 56 | def casts(pid) do 57 | GenServer.call(pid, {__MODULE__, :casts}) 58 | end 59 | 60 | def messages(pid) do 61 | GenServer.call(pid, {__MODULE__, :messages}) 62 | end 63 | 64 | def init(original) do 65 | state = %{ 66 | original: original, 67 | calls: [], 68 | casts: [], 69 | messages: [] 70 | } 71 | 72 | {:ok, state} 73 | end 74 | 75 | def handle_call({__MODULE__, key}, _from, state) do 76 | {:reply, Map.fetch(state, key), state} 77 | end 78 | 79 | def handle_call(call, _from, state) do 80 | response = GenServer.call(state.original, call) 81 | state = Map.update!(state, :calls, &[{call, response} | &1]) 82 | {:reply, response, state} 83 | end 84 | 85 | def handle_cast(cast, state) do 86 | GenServer.cast(state.original, cast) 87 | state = Map.update!(state, :casts, &[cast | &1]) 88 | {:noreply, state} 89 | end 90 | 91 | def handle_info(info, state) do 92 | send(state.original, info) 93 | state = Map.update!(state, :messages, &[info | &1]) 94 | {:noreply, state} 95 | end 96 | end 97 | 98 | ExUnit.start() 99 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"}, 3 | "earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm", "b42a23e9bd92d65d16db2f75553982e58519054095356a418bb8320bbacb58b1"}, 4 | "earmark_parser": {:hex, :earmark_parser, "1.4.26", "f4291134583f373c7d8755566122908eb9662df4c4b63caa66a0eabe06569b0a", [:mix], [], "hexpm", "48d460899f8a0c52c5470676611c01f64f3337bad0b26ddab43648428d94aabc"}, 5 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 6 | "ex_doc": {:hex, :ex_doc, "0.28.5", "3e52a6d2130ce74d096859e477b97080c156d0926701c13870a4e1f752363279", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "d2c4b07133113e9aa3e9ba27efb9088ba900e9e51caa383919676afdf09ab181"}, 7 | "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, 8 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, 9 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 10 | "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | name: Build and test 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | include: 17 | - elixir-version: 1.6.6 18 | otp-version: 20.3 19 | - elixir-version: 1.6.6 20 | otp-version: 21.3 21 | - elixir-version: 1.7.4 22 | otp-version: 20.3 23 | - elixir-version: 1.7.4 24 | otp-version: 21.3 25 | - elixir-version: 1.7.4 26 | otp-version: 22.3 27 | - elixir-version: 1.8.2 28 | otp-version: 20.3 29 | - elixir-version: 1.8.2 30 | otp-version: 21.3 31 | - elixir-version: 1.8.2 32 | otp-version: 22.3 33 | - elixir-version: 1.9.4 34 | otp-version: 20.3 35 | - elixir-version: 1.9.4 36 | otp-version: 21.3 37 | - elixir-version: 1.9.4 38 | otp-version: 22.3 39 | - elixir-version: 1.10.4 40 | otp-version: 21.3 41 | - elixir-version: 1.10.4 42 | otp-version: 22.3 43 | - elixir-version: 1.10.4 44 | otp-version: 23.2 45 | - elixir-version: 1.11.4 46 | otp-version: 21.3 47 | - elixir-version: 1.11.4 48 | otp-version: 22.3 49 | - elixir-version: 1.11.4 50 | otp-version: 23.2 51 | - elixir-version: 1.12.3 52 | otp-version: 22.3 53 | - elixir-version: 1.12.3 54 | otp-version: 23.3 55 | - elixir-version: 1.12.3 56 | otp-version: 24.3 57 | - elixir-version: 1.13.4 58 | otp-version: 22.3 59 | - elixir-version: 1.13.4 60 | otp-version: 23.3 61 | - elixir-version: 1.13.4 62 | otp-version: 24.3 63 | - elixir-version: 1.13.4 64 | otp-version: 25.1 65 | - elixir-version: 1.14.0 66 | otp-version: 23.3 67 | - elixir-version: 1.14.0 68 | otp-version: 24.3 69 | - elixir-version: 1.14.0 70 | otp-version: 25.1 71 | steps: 72 | - uses: actions/checkout@v2 73 | - name: Set up Elixir 74 | uses: erlef/setup-beam@v1 75 | with: 76 | elixir-version: ${{ matrix.elixir-version }} 77 | otp-version: ${{ matrix.otp-version }} 78 | - name: Restore dependencies cache 79 | uses: actions/cache@v2 80 | with: 81 | path: deps 82 | key: ${{ runner.os }}-${{ matrix.elixir-version }}-${{ matrix.otp-version}}-mix-${{ hashFiles('**/mix.lock') }} 83 | restore-keys: ${{ runner.os }}-${{ matrix.elixir-version}}-${{ matrix.otp-version }}-mix- 84 | - name: Install dependencies 85 | run: mix deps.get 86 | - name: Run tests 87 | run: mix test 88 | -------------------------------------------------------------------------------- /test/supervisor_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Supervisor.Test do 2 | use ExUnit.Case 3 | 4 | describe "pre-1.5 specs" do 5 | test "valid spec" do 6 | children = [ 7 | GenRegistry.Spec.child_spec(ExampleWorker) 8 | ] 9 | 10 | {:ok, supervisor_pid} = Supervisor.start_link(children, strategy: :one_for_one) 11 | 12 | assert Supervisor.count_children(supervisor_pid) == %{ 13 | active: 1, 14 | specs: 1, 15 | supervisors: 1, 16 | workers: 0 17 | } 18 | 19 | assert [{ExampleWorker, _, :supervisor, _}] = Supervisor.which_children(supervisor_pid) 20 | 21 | Supervisor.stop(supervisor_pid) 22 | end 23 | 24 | test "can customize the name and run multiple registries for the same module" do 25 | children = [ 26 | GenRegistry.Spec.child_spec(ExampleWorker, name: ExampleWorker.A), 27 | GenRegistry.Spec.child_spec(ExampleWorker, name: ExampleWorker.B) 28 | ] 29 | 30 | {:ok, supervisor_pid} = Supervisor.start_link(children, strategy: :one_for_one) 31 | 32 | assert Supervisor.count_children(supervisor_pid) == %{ 33 | active: 2, 34 | specs: 2, 35 | supervisors: 2, 36 | workers: 0 37 | } 38 | 39 | children = Supervisor.which_children(supervisor_pid) 40 | 41 | assert Enum.find(children, &match?({ExampleWorker.A, _, :supervisor, _}, &1)) 42 | assert Enum.find(children, &match?({ExampleWorker.B, _, :supervisor, _}, &1)) 43 | 44 | Supervisor.stop(supervisor_pid) 45 | end 46 | end 47 | 48 | describe "modern specs" do 49 | test "invalid spec, no arguments" do 50 | assert_raise KeyError, "key :worker_module not found in: []", fn -> 51 | children = [ 52 | GenRegistry 53 | ] 54 | 55 | Supervisor.start_link(children, strategy: :one_for_one) 56 | end 57 | end 58 | 59 | test "invalid spec, no :worker_module argument" do 60 | assert_raise KeyError, "key :worker_module not found in: [test_key: :test_value]", fn -> 61 | children = [ 62 | {GenRegistry, test_key: :test_value} 63 | ] 64 | 65 | Supervisor.start_link(children, strategy: :one_for_one) 66 | end 67 | end 68 | 69 | test "valid spec" do 70 | children = [ 71 | {GenRegistry, worker_module: ExampleWorker} 72 | ] 73 | 74 | {:ok, supervisor_pid} = Supervisor.start_link(children, strategy: :one_for_one) 75 | 76 | assert Supervisor.count_children(supervisor_pid) == %{ 77 | active: 1, 78 | specs: 1, 79 | supervisors: 1, 80 | workers: 0 81 | } 82 | 83 | assert [{ExampleWorker, _, :supervisor, _}] = Supervisor.which_children(supervisor_pid) 84 | 85 | Supervisor.stop(supervisor_pid) 86 | end 87 | 88 | test "can customize the name and run multiple registries for the same module" do 89 | children = [ 90 | {GenRegistry, worker_module: ExampleWorker, name: ExampleWorker.A}, 91 | {GenRegistry, worker_module: ExampleWorker, name: ExampleWorker.B} 92 | ] 93 | 94 | {:ok, supervisor_pid} = Supervisor.start_link(children, strategy: :one_for_one) 95 | 96 | assert Supervisor.count_children(supervisor_pid) == %{ 97 | active: 2, 98 | specs: 2, 99 | supervisors: 2, 100 | workers: 0 101 | } 102 | 103 | children = Supervisor.which_children(supervisor_pid) 104 | 105 | assert Enum.find(children, &match?({ExampleWorker.A, _, :supervisor, _}, &1)) 106 | assert Enum.find(children, &match?({ExampleWorker.B, _, :supervisor, _}, &1)) 107 | 108 | Supervisor.stop(supervisor_pid) 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GenRegistry 2 | 3 | [![CI](https://github.com/discord/gen_registry/workflows/CI/badge.svg)](https://github.com/discord/gen_registry/actions) 4 | [![Hex.pm Version](http://img.shields.io/hexpm/v/gen_registry.svg?style=flat)](https://hex.pm/packages/gen_registry) 5 | [![Hex.pm License](http://img.shields.io/hexpm/l/gen_registry.svg?style=flat)](https://hex.pm/packages/gen_registry) 6 | [![HexDocs](https://img.shields.io/badge/HexDocs-Yes-blue)](https://hexdocs.pm/gen_registry) 7 | 8 | `GenRegistry` provides a simple interface for managing a local registry of processes. 9 | 10 | ## Installation 11 | 12 | Add `GenRegistry` to your dependencies. 13 | 14 | ```elixir 15 | def deps do 16 | [ 17 | {:gen_registry, "~> 1.3.0"} 18 | ] 19 | end 20 | ``` 21 | 22 | ## Why GenRegistry? 23 | 24 | `GenRegistry` makes it easy to manage one process per id, this allows the application code to work with a more natural id (user_id, session_id, phone_number, etc), but still easily retrieve and lazily spawn processes. 25 | 26 | ### Example: Phone Number Denylist 27 | 28 | In our example we have some arbitrary spam deflection system where each phone number is allowed to define its own custom rules. To make sure our application stays simple we encapsulate the denylisting logic in a GenServer which handles caching, loading, and saving denylist rules. 29 | 30 | With `GenRegistry` we don't need to worry about carefully keeping track of Denylist pids for each phone number. The phone number (normalized) makes a natural id for the `GenRegistry`. `GenRegistry` will also manage the lifecycle and supervision of these processes, allowing us to write simplified code like this. 31 | 32 | ```elixir 33 | def place_call(sender, recipient) do 34 | # Get the recipients Denylist GenServer 35 | {:ok, denylist} = GenRegistry.lookup_or_start(Denylist, recipient, [recipient]) 36 | 37 | if Denylist.allow?(denylist, sender) do 38 | {:ok, :place_call} 39 | else 40 | {:error, :reject_call} 41 | end 42 | end 43 | ``` 44 | 45 | Does the recipient have a GenServer already running? If so then the pid will be returned, if not then `Denylist.start_link(recipient)` will be called, the resulting pid will be registered with the `GenRegistry` under the recipient phone number and the pid returned. 46 | 47 | ## Supervising the GenRegistry 48 | 49 | `GenRegistry` works best as part of a supervision tree. `GenRegistry` provides support for pre-1.5 `Supervisor.Spec` style child specs and the newer module based child specs. 50 | 51 | ### Module-Based Child Spec 52 | 53 | Introduced in Elixir 1.5, module-based child specs are the preferred way of defining a Supervisor's children. `GenRegistry.child_spec/1` is compatible with module-based child specs. 54 | 55 | Here's how to add a supervised `GenRegistry` to your application's `Supervisor` that will manage an example module called `ExampleWorker`. 56 | 57 | ```elixir 58 | def children do 59 | [ 60 | {GenRegistry, worker_module: ExampleWorker} 61 | ] 62 | end 63 | ``` 64 | 65 | `worker_module` is required, any other arguments will be used as the options for `GenServer.start_link/3` when starting the `GenRegistry` 66 | 67 | ### Supervisor.Spec Child Spec 68 | 69 | _This style was deprecated in Elixir 1.5, this functionality is provided for backwards 70 | compatibility._ 71 | 72 | If pre-1.5 style child specs are needed, the `GenRegistry.Spec` module provides a helper `GenRegistry.Spec.child_spec/2` which will generate the appropriate spec to manage a `GenRegistry` process. 73 | 74 | Here's how to add a supervised `GenRegistry` to your application's `Supervisor` that will manage an example module called `ExampleWorker`. 75 | 76 | ```elixir 77 | def children do 78 | [ 79 | GenRegistry.Spec.child_spec(ExampleWorker) 80 | ] 81 | end 82 | ``` 83 | 84 | The second argument is a `Keyword` of options that will be passed as the options for `GenServer.start_link/3` when starting the `GenRegistry` 85 | 86 | ## Basic Usage 87 | 88 | `GenRegistry` uses some conventions to make it easy to work with. `GenRegistry` will use `GenServer.start_link/3`'s `:name` facility to give the `GenRegistry` the same name as the `worker_module`. `GenRegistry` will also name the `ETS` table after the `worker_module`. 89 | 90 | These two conventions together mean almost every function in the `GenRegistry` API can be called with the `worker_module` as the first argument and work as expected. 91 | 92 | Building off of the above supervision section, let's assume that we've start a `GenRegistry` to manage the `ExampleWorker` module. 93 | 94 | ### Customizing the Name 95 | 96 | The conventions covered in the last section work for most cases but what happens if you want to use the same `worker_module` for multiple logical registries. This is where custom names come in. 97 | 98 | Simply provide a `name` argument. 99 | 100 | For pre-1.5 child specifications 101 | 102 | ```elixir 103 | def children do 104 | [ 105 | GenRegistry.Spec.child_spec(ExampleWorker, name: ExampleWorker.Read) 106 | GenRegistry.Spec.child_spec(ExampleWorker, name: ExampleWorker.Write) 107 | ] 108 | end 109 | ``` 110 | 111 | For Module-Based child specifications 112 | 113 | ```elixir 114 | def children do 115 | [ 116 | {GenRegistry, worker_module: ExampleWorker, name: ExampleWorker.Read}, 117 | {GenRegistry, worker_module: ExampleWorker, name: ExampleWorker.Write}, 118 | ] 119 | end 120 | ``` 121 | 122 | Now simply use the `name` in place of the `worker_module` when calling any of the functions outlined below. 123 | ### Starting or retrieving a process 124 | 125 | `GenRegistry` makes it easy to idempotently start worker processes using the `GenRegistry.lookup_or_start/4` function. 126 | 127 | ```elixir 128 | {:ok, pid} = GenRegistry.lookup_or_start(ExampleWorker, :example_id) 129 | ``` 130 | 131 | If `:example_id` is already bound to a running process, then that process's pid is returned. Otherwise a new process is started. Workers are started by calling the `worker_module`'s `start_link` function, `GenRegistry.lookup_or_start/4` accepts an optional third argument, a list of arguments to pass to `start_link`, the default is to pass no arguments. 132 | 133 | If there is an error spawning a new process, `{:error, reason}` is returned. 134 | 135 | ### Starting a new process 136 | 137 | Sometimes a process will want to know about an associated pid but only if it is the starting process. The `GenRegistry.start/4` function is similar to `GenRegistry.lookup_or_start/4` but it will return an `{:error, {:already_started, pid}}` if the id is already associated with a running process. 138 | 139 | This can be useful when the starting process acts like an owner and a single-owner rule is desired. 140 | 141 | If there is not process associated with the provided id then one will be started by calling the `worker_module`'s `start_link` function. Just like `GenRegistry.lookup_or_start/4`, `GenRegistry.start/4` accepts an optional third arguments, a list of arguments to pass to `start_link`, the default is to pass no arguments. 142 | 143 | If there is an error spawning a new process `{:error, reason}` is returned. 144 | 145 | ### Retrieving the pid for an id 146 | 147 | `GenRegistry` makes it easy to get the pid associated with an id using the `GenRegistry.lookup/2` function. 148 | 149 | This function reads from the ETS table in the current process's context avoiding a GenServer call. 150 | 151 | ```elixir 152 | case GenRegistry.lookup(ExampleWorker, :example_id) do 153 | {:ok, pid} -> 154 | # Do something interesting with pid 155 | 156 | {:error, :not_found} -> 157 | # :example_id isn't bound to any running process. 158 | end 159 | ``` 160 | 161 | ### Stopping a process by id 162 | 163 | `GenRegistry` also supports stopping a child process using the `GenRegistry.stop/2` function. 164 | 165 | ```elixir 166 | case GenRegistry.stop(ExampleWorker, :example_id) do 167 | :ok -> 168 | # Process was successfully stopped 169 | 170 | {:error, :not_found} -> 171 | # :example_id isn't bound to any running process. 172 | end 173 | ``` 174 | 175 | ### Counting processes 176 | 177 | `GenRegistry` manages all the processes it has spawned, it is capable of reporting how many processes it is currently managing using the `GenRegistry.count/1` function. 178 | 179 | ```elixir 180 | IO.puts("There are #{GenRegistry.count(ExampleWorker)} ExampleWorker processes") 181 | ``` 182 | 183 | ### Bulk Operations 184 | 185 | `GenRegistry` provides a facility for reducing a function over every process using the `GenRegistry.reduce/3` function. This function accepts an accumulator and ultimately returns the accumulator. 186 | 187 | ```elixir 188 | {max_id, max_pid} = 189 | GenRegistry.reduce(ExampleWorker, {nil, -1}, fn 190 | {id, pid}, {_, current}=acc -> 191 | value = ExampleWorker.foobars(pid) 192 | if value > current do 193 | {id, pid} 194 | else 195 | acc 196 | end 197 | end) 198 | ``` 199 | 200 | ### More Details 201 | 202 | All the methods of GenRegistry are documented and spec'd, the tests also provide example code for how to use GenRegistry. 203 | 204 | ## Configuration 205 | 206 | `GenRegistry` only has a single configuration setting, `:gen_registry` `:gen_module`. This is the module to use to when performing `GenServer` calls, it defaults to `GenServer`. 207 | 208 | ## Documentation 209 | 210 | Documentation is [hosted on hexdocs](https://hexdocs.pm/gen_registry). 211 | 212 | ## Running the Tests 213 | 214 | GenRegistry ships with a full suite of tests, these are normal ExUnit tests. 215 | 216 | ```console 217 | $ mix tests 218 | ``` 219 | -------------------------------------------------------------------------------- /lib/gen_registry.ex: -------------------------------------------------------------------------------- 1 | defmodule GenRegistry do 2 | @moduledoc """ 3 | GenRegistry provides a `Registry` like interface for managing processes. 4 | """ 5 | 6 | @behaviour GenRegistry.Behaviour 7 | 8 | use GenServer 9 | 10 | alias :ets, as: ETS 11 | alias GenRegistry.Types 12 | 13 | defstruct [:worker_module, :worker_type, :workers] 14 | 15 | @typedoc """ 16 | GenRegistry State. 17 | 18 | - worker_module: Module to spawn 19 | - worker_type: `:supervisor` if the worker_module is a supervisor, `:worker` otherwise 20 | - workers: ETS table id holding the worker tracking records. 21 | """ 22 | @type t :: %__MODULE__{ 23 | worker_module: module, 24 | worker_type: :supervisor | :worker, 25 | workers: ETS.tab() 26 | } 27 | 28 | @gen_module Application.get_env(:gen_registry, :gen_module, GenServer) 29 | 30 | ## Client 31 | 32 | @doc """ 33 | Callback called by `Supervisor.init/2` 34 | 35 | It is required that you provide a `:worker_module` argument or the call will fail. 36 | """ 37 | @spec child_spec(opts :: Keyword.t()) :: Supervisor.child_spec() 38 | def child_spec(opts) do 39 | worker_module = Keyword.fetch!(opts, :worker_module) 40 | 41 | opts = 42 | opts 43 | |> Keyword.delete(:worker_module) 44 | |> Keyword.put_new(:name, worker_module) 45 | 46 | %{ 47 | id: opts[:name], 48 | start: {__MODULE__, :start_link, [worker_module, opts]}, 49 | type: :supervisor 50 | } 51 | end 52 | 53 | @doc """ 54 | Start a registry instance. 55 | 56 | GenRegistry should be run under a supervision tree, it is not recommended to call this directly. 57 | """ 58 | @spec start_link(module, Keyword.t()) :: {:ok, pid} | {:error, any} 59 | def start_link(module, opts \\ []) do 60 | GenServer.start_link(__MODULE__, {module, opts[:name]}, opts) 61 | end 62 | 63 | @doc """ 64 | Lookup a running a process. 65 | 66 | This is a fast path to the ETS table. 67 | """ 68 | @spec lookup(table :: ETS.tab(), id :: Types.id()) :: {:ok, pid} | {:error, :not_found} 69 | def lookup(table, id) do 70 | case ETS.lookup(table, id) do 71 | [{^id, pid}] -> {:ok, pid} 72 | [] -> {:error, :not_found} 73 | end 74 | end 75 | 76 | @doc """ 77 | Attempts to lookup a running process by id. 78 | 79 | If the id is not associated with a running process then it is spawned, the optional third 80 | argument will be passed to `start_link` of the `worker_module` to spawn a new process. 81 | """ 82 | @spec lookup_or_start( 83 | registry :: GenServer.server(), 84 | id :: Types.id(), 85 | args :: [any], 86 | timeout :: integer 87 | ) :: 88 | {:ok, pid} | {:error, any} 89 | def lookup_or_start(registry, id, args \\ [], timeout \\ 5_000) 90 | 91 | def lookup_or_start(registry, id, args, timeout) when is_atom(registry) do 92 | case lookup(registry, id) do 93 | {:ok, pid} -> 94 | {:ok, pid} 95 | 96 | {:error, :not_found} -> 97 | @gen_module.call(registry, {:lookup_or_start, id, args}, timeout) 98 | end 99 | end 100 | 101 | def lookup_or_start(registry, id, args, timeout) do 102 | @gen_module.call(registry, {:lookup_or_start, id, args}, timeout) 103 | end 104 | 105 | @doc """ 106 | Starts a process by id 107 | 108 | If the id is already associated with a running process `{:error, {:already_started, pid}}` is 109 | returned. 110 | 111 | If the id is not associated with a running process then it is spawned, the optional third 112 | argument will be passed to `start_link` of the `worker_module` to spawn a new process. 113 | """ 114 | @spec start( 115 | registry :: GenServer.server(), 116 | id :: Types.id(), 117 | args :: [any()], 118 | timeout :: integer() 119 | ) :: {:ok, pid()} | {:error, {:already_started, pid()}} | {:error, any()} 120 | def start(registry, id, args \\ [], timeout \\ 5_000) 121 | 122 | def start(registry, id, args, timeout) when is_atom(registry) do 123 | case lookup(registry, id) do 124 | {:ok, pid} -> 125 | {:error, {:already_started, pid}} 126 | 127 | {:error, :not_found} -> 128 | @gen_module.call(registry, {:start, id, args}, timeout) 129 | end 130 | end 131 | 132 | def start(registry, id, args, timeout) do 133 | @gen_module.call(registry, {:start, id, args}, timeout) 134 | end 135 | 136 | @doc """ 137 | Safely stops a process managed by the GenRegistry 138 | 139 | In addition to stopping the process, the id is also removed from the GenRegistry 140 | 141 | If the id provided is not registered this will return `{:error, :not_found}` 142 | """ 143 | @spec stop(registry :: GenServer.server(), id :: Types.id()) :: :ok | {:error, :not_found} 144 | def stop(registry, id) do 145 | @gen_module.call(registry, {:stop, id}) 146 | end 147 | 148 | @doc """ 149 | Return the number of running processes in this registry. 150 | """ 151 | @spec count(table :: ETS.tab()) :: non_neg_integer() 152 | def count(table) do 153 | ETS.info(table, :size) 154 | end 155 | 156 | @doc """ 157 | Return a sample entry from the registry. 158 | 159 | If the registry is empty returns `nil`. 160 | """ 161 | @spec sample(table :: ETS.tab()) :: {Types.id(), pid()} | nil 162 | def sample(table) do 163 | case ETS.first(table) do 164 | :"$end_of_table" -> 165 | nil 166 | 167 | key -> 168 | case ETS.lookup(table, key) do 169 | [entry] -> 170 | entry 171 | 172 | _ -> 173 | sample(table) 174 | end 175 | end 176 | end 177 | 178 | @doc """ 179 | Loop over all the processes and return result. 180 | 181 | The function will be called with two arguments, a two-tuple of `{id, pid}` and then accumulator, 182 | the function should return the accumulator. 183 | 184 | There is no ordering guarantee when reducing. 185 | """ 186 | @spec reduce(table :: ETS.tab(), acc :: any, ({Types.id(), pid()}, any() -> any())) :: any 187 | def reduce(table, acc, func) do 188 | ETS.foldr(func, acc, table) 189 | end 190 | 191 | @doc """ 192 | Returns all the entries of the GenRegistry as a list. 193 | 194 | There is no ordering guarantee for the list. 195 | """ 196 | @spec to_list(table :: ETS.tab()) :: [{Types.id(), pid()}] 197 | def to_list(table) do 198 | ETS.tab2list(table) 199 | end 200 | 201 | ## Server Callbacks 202 | 203 | def init({worker_module, name}) do 204 | Process.flag(:trap_exit, true) 205 | 206 | worker_type = 207 | case worker_module.module_info[:attributes][:behaviour] do 208 | [:supervisor] -> :supervisor 209 | _ -> :worker 210 | end 211 | 212 | state = %__MODULE__{ 213 | workers: 214 | ETS.new(name, [ 215 | :public, 216 | :set, 217 | :named_table, 218 | {:read_concurrency, true} 219 | ]), 220 | worker_module: worker_module, 221 | worker_type: worker_type 222 | } 223 | 224 | {:ok, state} 225 | end 226 | 227 | def terminate(_reason, _state) do 228 | for pid <- Process.get_keys(), is_pid(pid) do 229 | Process.unlink(pid) 230 | Process.exit(pid, :kill) 231 | end 232 | 233 | :ok 234 | end 235 | 236 | def handle_call({:lookup_or_start, id, args}, _from, state) do 237 | {:reply, do_lookup_or_start(state, id, args), state} 238 | end 239 | 240 | def handle_call({:start, id, args}, _from, state) do 241 | {:reply, do_start(state, id, args), state} 242 | end 243 | 244 | def handle_call({:stop, id}, _from, state) do 245 | {:reply, do_stop(state, id), state} 246 | end 247 | 248 | # Call from supervisor module. 249 | def handle_call( 250 | :which_children, 251 | _from, 252 | %__MODULE__{worker_type: worker_type, worker_module: worker_module} = state 253 | ) do 254 | children = 255 | for pid <- Process.get_keys(), is_pid(pid) do 256 | {:undefined, pid, worker_type, [worker_module]} 257 | end 258 | 259 | {:reply, children, state} 260 | end 261 | 262 | def handle_call( 263 | :count_children, 264 | _from, 265 | %__MODULE__{worker_type: worker_type, worker_module: worker_module} = state 266 | ) do 267 | counts = [ 268 | specs: 1, 269 | active: count(worker_module), 270 | supervisors: 0, 271 | workers: 0 272 | ] 273 | 274 | counts = 275 | case worker_type do 276 | :worker -> Keyword.put(counts, :workers, counts[:active]) 277 | :supervisor -> Keyword.put(counts, :supervisors, counts[:active]) 278 | end 279 | 280 | {:reply, counts, state} 281 | end 282 | 283 | def handle_call(_message, _from, state) do 284 | {:reply, {:error, __MODULE__}, state} 285 | end 286 | 287 | def handle_info({:EXIT, pid, _reason}, %__MODULE__{workers: workers} = state) do 288 | ETS.delete(workers, Process.delete(pid)) 289 | {:noreply, state} 290 | end 291 | 292 | def handle_info(_message, state) do 293 | {:noreply, state} 294 | end 295 | 296 | ## Private 297 | 298 | @spec do_lookup_or_start(state :: t, id :: Types.id(), args :: [any]) :: 299 | {:ok, pid} | {:error, any} 300 | defp do_lookup_or_start(%__MODULE__{worker_module: worker_module, workers: workers}, id, args) do 301 | case lookup(workers, id) do 302 | {:ok, pid} -> 303 | {:ok, pid} 304 | 305 | {:error, :not_found} -> 306 | case apply(worker_module, :start_link, args) do 307 | {:ok, pid} -> 308 | ETS.insert_new(workers, {id, pid}) 309 | Process.put(pid, id) 310 | {:ok, pid} 311 | 312 | {:error, reason} -> 313 | {:error, reason} 314 | end 315 | end 316 | end 317 | 318 | @spec do_start(state :: t(), id :: Types.id(), args :: [any()]) :: 319 | {:ok, pid()} | {:error, {:already_started, pid()}} | {:error, any()} 320 | defp do_start(%__MODULE__{worker_module: worker_module, workers: workers}, id, args) do 321 | case lookup(workers, id) do 322 | {:ok, pid} -> 323 | {:error, {:already_started, pid}} 324 | 325 | {:error, :not_found} -> 326 | case apply(worker_module, :start_link, args) do 327 | {:ok, pid} -> 328 | ETS.insert_new(workers, {id, pid}) 329 | Process.put(pid, id) 330 | {:ok, pid} 331 | 332 | {:error, reason} -> 333 | {:error, reason} 334 | end 335 | end 336 | end 337 | 338 | @spec do_stop(state :: t, id :: Types.id()) :: :ok | {:error, :not_found} 339 | defp do_stop(%__MODULE__{workers: workers}, id) do 340 | with {:ok, pid} <- lookup(workers, id) do 341 | Process.unlink(pid) 342 | Process.exit(pid, :shutdown) 343 | ETS.delete(workers, Process.delete(pid)) 344 | :ok 345 | end 346 | end 347 | end 348 | -------------------------------------------------------------------------------- /test/gen_registry_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GenRegistry.Test do 2 | use ExUnit.Case 3 | 4 | @doc """ 5 | Common setup function that starts a GenRegistry 6 | 7 | In this test context the ExUnit Supervisor will supervise the GenRegistry. The GenRegistry 8 | process is registered with the the name of the worker module, this is the default behavior for 9 | running GenRegistry under supervision so the resulting test code mimics how one would use 10 | GenRegistry in practice. 11 | """ 12 | @spec start_registry(ctx :: Map.t()) :: :ok 13 | def start_registry(_) do 14 | {:ok, registry} = start_supervised({GenRegistry, worker_module: ExampleWorker}) 15 | {:ok, registry: registry} 16 | end 17 | 18 | @doc """ 19 | Helper that executes a function until it returns true 20 | 21 | Useful for operations that will eventually complete, instead of sleeping to allow an async 22 | operation to complete, wait_until will call the function in a loop up to the specified number of 23 | attempts with the specified delay between attempts. 24 | """ 25 | @spec wait_until(fun :: (() -> boolean), attempts :: non_neg_integer, delay :: pos_integer) :: 26 | boolean 27 | def wait_until(fun, attempts \\ 5, delay \\ 100) 28 | 29 | def wait_until(_, 0, _), do: false 30 | 31 | def wait_until(fun, attempts, delay) do 32 | case fun.() do 33 | true -> 34 | true 35 | 36 | _ -> 37 | Process.sleep(delay) 38 | wait_until(fun, attempts - 1, delay) 39 | end 40 | end 41 | 42 | describe "lookup/2" do 43 | setup [:start_registry] 44 | 45 | test "unknown id" do 46 | assert {:error, :not_found} = GenRegistry.lookup(ExampleWorker, :unknown) 47 | end 48 | 49 | test "known id" do 50 | # Start a process 51 | assert {:ok, pid} = GenRegistry.lookup_or_start(ExampleWorker, :test_id) 52 | 53 | # Assert that it can be looked up 54 | assert {:ok, ^pid} = GenRegistry.lookup(ExampleWorker, :test_id) 55 | end 56 | 57 | test "mixed ids" do 58 | # Start a process 59 | assert {:ok, pid} = GenRegistry.lookup_or_start(ExampleWorker, :test_id) 60 | 61 | # Assert that it can be looked up 62 | assert {:ok, ^pid} = GenRegistry.lookup(ExampleWorker, :test_id) 63 | 64 | # Assert that other ids are still not found 65 | assert {:error, :not_found} = GenRegistry.lookup(ExampleWorker, :unknown) 66 | end 67 | 68 | test "removed after the process exits" do 69 | # Start a new process 70 | assert {:ok, pid} = GenRegistry.lookup_or_start(ExampleWorker, :test_id) 71 | 72 | # Assert that it can be looked up 73 | assert {:ok, ^pid} = GenRegistry.lookup(ExampleWorker, :test_id) 74 | 75 | # Stop the process 76 | assert :ok = GenRegistry.stop(ExampleWorker, :test_id) 77 | 78 | # Assert that the previously known id is no longer known 79 | assert {:error, :not_found} = GenRegistry.lookup(ExampleWorker, :test_id) 80 | end 81 | end 82 | 83 | describe "lookup_or_start/4" do 84 | setup [:start_registry] 85 | 86 | test "unknown id starts a new process" do 87 | assert {:ok, pid} = GenRegistry.lookup_or_start(ExampleWorker, :unknown) 88 | assert {:ok, ^pid} = GenRegistry.lookup(ExampleWorker, :unknown) 89 | end 90 | 91 | test "known id returns the running process" do 92 | # Confirm that the registry is empty 93 | assert 0 == GenRegistry.count(ExampleWorker) 94 | 95 | # Start a new process 96 | assert {:ok, pid} = GenRegistry.lookup_or_start(ExampleWorker, :test_id) 97 | 98 | # Confirm that the registry has 1 process 99 | assert 1 == GenRegistry.count(ExampleWorker) 100 | 101 | # Attempting to start the same id will return the running process 102 | assert {:ok, ^pid} = GenRegistry.lookup_or_start(ExampleWorker, :test_id) 103 | 104 | # Confirm that the registry still only has 1 process 105 | assert 1 == GenRegistry.count(ExampleWorker) 106 | end 107 | 108 | test "arguments are passed through" do 109 | # Start a process with a particular value 110 | assert {:ok, first} = GenRegistry.lookup_or_start(ExampleWorker, :a, [:test_value_a]) 111 | 112 | # Start another process with a different value 113 | assert {:ok, second} = GenRegistry.lookup_or_start(ExampleWorker, :b, [:test_value_b]) 114 | 115 | # Assert that the first process has the first value 116 | assert :test_value_a == ExampleWorker.get(first) 117 | 118 | # Assert that the second process has the second value 119 | assert :test_value_b == ExampleWorker.get(second) 120 | 121 | # Assert that querying through the returned lookup pid returns the correct value, without 122 | # having to pass the arguments in again 123 | assert {:ok, retrieved} = GenRegistry.lookup_or_start(ExampleWorker, :a) 124 | assert :test_value_a = ExampleWorker.get(retrieved) 125 | end 126 | 127 | test "invalid arguments return the error and register no process" do 128 | assert {:error, :invalid} = GenRegistry.lookup_or_start(ExampleWorker, :test_id, [:invalid]) 129 | end 130 | 131 | test "invalid start does not effect running processes (failure isolation)" do 132 | # Populate the registry with some processes 133 | for i <- 1..5 do 134 | assert {:ok, _} = GenRegistry.lookup_or_start(ExampleWorker, i) 135 | end 136 | 137 | # Confirm that there are 5 processes 138 | assert 5 == GenRegistry.count(ExampleWorker) 139 | 140 | # Start a process with invalid arguments 141 | assert {:error, :invalid} = GenRegistry.lookup_or_start(ExampleWorker, :test_id, [:invalid]) 142 | 143 | # Confirm that there are still 5 processes in the registry 144 | assert 5 == GenRegistry.count(ExampleWorker) 145 | 146 | # Confirm that the running processes are unaffected by the failed start 147 | for i <- 1..5 do 148 | assert {:ok, pid} = GenRegistry.lookup(ExampleWorker, i) 149 | assert Process.alive?(pid) 150 | end 151 | end 152 | 153 | test "id can be reused if the process exits" do 154 | # Start a process with a particular value 155 | assert {:ok, pid} = GenRegistry.lookup_or_start(ExampleWorker, :test_id, [:original]) 156 | 157 | # Confirm that the process is working correctly 158 | assert :original == ExampleWorker.get(pid) 159 | 160 | # Confirm that the registry has exactly 1 process 161 | assert 1 == GenRegistry.count(ExampleWorker) 162 | 163 | # Confirm that an additional lookup_or_start returns the running process 164 | assert {:ok, ^pid} = GenRegistry.lookup_or_start(ExampleWorker, :test_id) 165 | 166 | # Stop the process 167 | assert :ok = GenRegistry.stop(ExampleWorker, :test_id) 168 | 169 | # Confirm that the process has stopped 170 | refute Process.alive?(pid) 171 | 172 | # Confirm that the registry has 0 processes 173 | assert 0 == GenRegistry.count(ExampleWorker) 174 | 175 | # Attempt to start up a new process with the same id 176 | assert {:ok, new_pid} = GenRegistry.lookup_or_start(ExampleWorker, :test_id, [:updated]) 177 | 178 | # Confirm that the process is distinct from the first process 179 | assert pid != new_pid 180 | 181 | # Confirm that the process is working correctly 182 | assert :updated == ExampleWorker.get(new_pid) 183 | 184 | # Confirm that the registry has 1 process 185 | assert 1 == GenRegistry.count(ExampleWorker) 186 | end 187 | 188 | test "when given a local name will perform a client-side lookup" do 189 | # Start a process so something will be available client-side 190 | assert {:ok, pid} = GenRegistry.lookup_or_start(ExampleWorker, :test_id, [:test]) 191 | 192 | # Replace the GenRegistry with a Spy 193 | assert {:ok, spy} = Spy.replace(ExampleWorker) 194 | 195 | # Assert that the spy has seen 0 calls 196 | assert {:ok, []} == Spy.calls(spy) 197 | 198 | # Assert that the GenRegistry can lookup the `:test_id` process 199 | assert {:ok, pid} == GenRegistry.lookup_or_start(ExampleWorker, :test_id, [:test]) 200 | 201 | # Assert again that the spy has seen 0 calls 202 | assert {:ok, []} == Spy.calls(spy) 203 | end 204 | 205 | test "when given a local name will perform a server-side lookup and start if id not found" do 206 | # Replace the GenRegistry with a Spy 207 | assert {:ok, spy} = Spy.replace(ExampleWorker) 208 | 209 | # Assert that the spy has seen 0 calls 210 | assert {:ok, []} == Spy.calls(spy) 211 | 212 | # Assert that the GenRegistry can still perform a lookup_or_start 213 | assert {:ok, pid} = GenRegistry.lookup_or_start(ExampleWorker, :test_id, [:test]) 214 | 215 | # Assert that the spy saw the call for lookup_or_start 216 | assert {:ok, [{call, response}]} = Spy.calls(spy) 217 | 218 | # Assert that the call is what we would expect for a server-side lookup_or_start 219 | assert {:lookup_or_start, :test_id, [:test]} == call 220 | 221 | # Assert that the response is the one returned from the call 222 | assert {:ok, pid} == response 223 | end 224 | 225 | test "when given a pid will perform a server-side lookup and start if id is running", ctx do 226 | # Start a process so something will be available client-side 227 | assert {:ok, pid} = GenRegistry.lookup_or_start(ExampleWorker, :test_id, [:test]) 228 | 229 | # Replace the GenRegistry with a Spy 230 | assert {:ok, spy} = Spy.replace(ctx.registry) 231 | 232 | # Assert that the spy has seen 0 calls 233 | assert {:ok, []} == Spy.calls(spy) 234 | 235 | # Assert that the GenRegistry can lookup the `:test_id` process 236 | assert {:ok, pid} == GenRegistry.lookup_or_start(spy, :test_id, [:test]) 237 | 238 | # Assert that the spy saw the call for lookup_or_start 239 | assert {:ok, [{call, response}]} = Spy.calls(spy) 240 | 241 | # Assert that the call is what we would expect for a server-side lookup_or_start 242 | assert {:lookup_or_start, :test_id, [:test]} == call 243 | 244 | # Assert that the response is the one returned from the call 245 | assert {:ok, pid} == response 246 | end 247 | 248 | test "when given a pid will perform server-side lookup and start if id is not running", ctx do 249 | # Replace the GenRegistry with a Spy 250 | assert {:ok, spy} = Spy.replace(ctx.registry) 251 | 252 | # Assert that the spy has seen 0 calls 253 | assert {:ok, []} == Spy.calls(spy) 254 | 255 | # Assert that the GenRegistry can lookup the `:test_id` process 256 | assert {:ok, pid} = GenRegistry.lookup_or_start(spy, :test_id, [:test]) 257 | 258 | # Assert that the spy saw the call for lookup_or_start 259 | assert {:ok, [{call, response}]} = Spy.calls(spy) 260 | 261 | # Assert that the call is what we would expect for a server-side lookup_or_start 262 | assert {:lookup_or_start, :test_id, [:test]} == call 263 | 264 | # Assert that the response is the one returned from the call 265 | assert {:ok, pid} == response 266 | end 267 | end 268 | 269 | describe "start/4" do 270 | setup [:start_registry] 271 | 272 | test "unknown id starts a new process" do 273 | assert {:ok, pid} = GenRegistry.start(ExampleWorker, :unknown) 274 | assert {:ok, ^pid} = GenRegistry.lookup(ExampleWorker, :unknown) 275 | end 276 | 277 | test "known id returns an error" do 278 | # Confirm that the registry is empty 279 | assert 0 == GenRegistry.count(ExampleWorker) 280 | 281 | # Start a new process 282 | assert {:ok, pid} = GenRegistry.start(ExampleWorker, :test_id) 283 | 284 | # Confirm that the registry has 1 process 285 | assert 1 == GenRegistry.count(ExampleWorker) 286 | 287 | # Attempting to start the same id will return an error 288 | assert {:error, {:already_started, ^pid}} = GenRegistry.start(ExampleWorker, :test_id) 289 | 290 | # Confirm that the registry still only has 1 process 291 | assert 1 == GenRegistry.count(ExampleWorker) 292 | end 293 | 294 | test "arguments are passed through" do 295 | # Start a process with a particular value 296 | assert {:ok, first} = GenRegistry.start(ExampleWorker, :a, [:test_value_a]) 297 | 298 | # Start another process with a different value 299 | assert {:ok, second} = GenRegistry.start(ExampleWorker, :b, [:test_value_b]) 300 | 301 | # Assert that the first process has the first value 302 | assert :test_value_a == ExampleWorker.get(first) 303 | 304 | # Assert that the second process has the second value 305 | assert :test_value_b == ExampleWorker.get(second) 306 | 307 | # Assert that querying through the returned lookup pid returns the correct value, without 308 | # having to pass the arguments in again 309 | assert {:ok, retrieved} = GenRegistry.lookup_or_start(ExampleWorker, :a) 310 | assert :test_value_a = ExampleWorker.get(retrieved) 311 | end 312 | 313 | test "invalid arguments return the error and register no process" do 314 | assert {:error, :invalid} = GenRegistry.start(ExampleWorker, :test_id, [:invalid]) 315 | end 316 | 317 | test "invalid start does not effect running processes (failure isolation)" do 318 | # Populate the registry with some processes 319 | for i <- 1..5 do 320 | assert {:ok, _} = GenRegistry.start(ExampleWorker, i) 321 | end 322 | 323 | # Confirm that there are 5 processes 324 | assert 5 == GenRegistry.count(ExampleWorker) 325 | 326 | # Start a process with invalid arguments 327 | assert {:error, :invalid} = GenRegistry.start(ExampleWorker, :test_id, [:invalid]) 328 | 329 | # Confirm that there are still 5 processes in the registry 330 | assert 5 == GenRegistry.count(ExampleWorker) 331 | 332 | # Confirm that the running processes are unaffected by the failed start 333 | for i <- 1..5 do 334 | assert {:ok, pid} = GenRegistry.lookup(ExampleWorker, i) 335 | assert Process.alive?(pid) 336 | end 337 | end 338 | 339 | test "id can be reused if the process exits" do 340 | # Start a process with a particular value 341 | assert {:ok, pid} = GenRegistry.start(ExampleWorker, :test_id, [:original]) 342 | 343 | # Confirm that the process is working correctly 344 | assert :original == ExampleWorker.get(pid) 345 | 346 | # Confirm that the registry has exactly 1 process 347 | assert 1 == GenRegistry.count(ExampleWorker) 348 | 349 | # Confirm that the running process causes an already_started error 350 | assert {:error, {:already_started, ^pid}} = 351 | GenRegistry.start(ExampleWorker, :test_id, [:duplicate]) 352 | 353 | # Confirm that the process is working correctly and returning the original value 354 | assert :original == ExampleWorker.get(pid) 355 | 356 | # Stop the process 357 | assert :ok = GenRegistry.stop(ExampleWorker, :test_id) 358 | 359 | # Confirm that the process has stopped 360 | refute Process.alive?(pid) 361 | 362 | # Confirm that the registry has 0 processes 363 | assert 0 == GenRegistry.count(ExampleWorker) 364 | 365 | # Attempt to start up a new process with the same id 366 | assert {:ok, new_pid} = GenRegistry.start(ExampleWorker, :test_id, [:updated]) 367 | 368 | # Confirm that the process is distinct from the first process 369 | assert pid != new_pid 370 | 371 | # Confirm that the process is working correctly 372 | assert :updated == ExampleWorker.get(new_pid) 373 | 374 | # Confirm that the registry has 1 process 375 | assert 1 == GenRegistry.count(ExampleWorker) 376 | end 377 | 378 | test "when given a local name will perform a client-side lookup" do 379 | # Start a process so something will be available client-side 380 | assert {:ok, pid} = GenRegistry.start(ExampleWorker, :test_id, [:test]) 381 | 382 | # Replace the GenRegistry with a Spy 383 | assert {:ok, spy} = Spy.replace(ExampleWorker) 384 | 385 | # Assert that the spy has seen 0 calls 386 | assert {:ok, []} == Spy.calls(spy) 387 | 388 | # Assert that the GenRegistry performs a client-side lookup the `:test_id` process 389 | assert {:error, {:already_started, pid}} == 390 | GenRegistry.start(ExampleWorker, :test_id, [:duplicate]) 391 | 392 | # Assert again that the spy has seen 0 calls 393 | assert {:ok, []} == Spy.calls(spy) 394 | end 395 | 396 | test "when given a local name will perform a server-side start if id not found" do 397 | # Replace the GenRegistry with a Spy 398 | assert {:ok, spy} = Spy.replace(ExampleWorker) 399 | 400 | # Assert that the spy has seen 0 calls 401 | assert {:ok, []} == Spy.calls(spy) 402 | 403 | # Assert that the GenRegistry can still perform a lookup_or_start 404 | assert {:ok, pid} = GenRegistry.start(ExampleWorker, :test_id, [:test]) 405 | 406 | # Assert that the spy saw the call for lookup_or_start 407 | assert {:ok, [{call, response}]} = Spy.calls(spy) 408 | 409 | # Assert that the call is what we would expect for a server-side start 410 | assert {:start, :test_id, [:test]} == call 411 | 412 | # Assert that the response is the one returned from the call 413 | assert {:ok, pid} == response 414 | end 415 | 416 | test "when given a pid will perform a server-side start if id is running", ctx do 417 | # Start a process so something will be available client-side 418 | assert {:ok, pid} = GenRegistry.start(ExampleWorker, :test_id, [:test]) 419 | 420 | # Replace the GenRegistry with a Spy 421 | assert {:ok, spy} = Spy.replace(ctx.registry) 422 | 423 | # Assert that the spy has seen 0 calls 424 | assert {:ok, []} == Spy.calls(spy) 425 | 426 | # Assert that the GenRegistry can lookup the `:test_id` process 427 | assert {:error, {:already_started, pid}} == GenRegistry.start(spy, :test_id, [:duplicate]) 428 | 429 | # Assert that the spy saw the call for lookup_or_start 430 | assert {:ok, [{call, response}]} = Spy.calls(spy) 431 | 432 | # Assert that the call is what we would expect for a server-side start 433 | assert {:start, :test_id, [:duplicate]} == call 434 | 435 | # Assert that the response is the one returned from the call 436 | assert {:error, {:already_started, pid}} == response 437 | end 438 | 439 | test "when given a pid will perform server-side start if id is not running", ctx do 440 | # Replace the GenRegistry with a Spy 441 | assert {:ok, spy} = Spy.replace(ctx.registry) 442 | 443 | # Assert that the spy has seen 0 calls 444 | assert {:ok, []} == Spy.calls(spy) 445 | 446 | # Assert that the GenRegistry can lookup the `:test_id` process 447 | assert {:ok, pid} = GenRegistry.start(spy, :test_id, [:test]) 448 | 449 | # Assert that the spy saw the call for lookup_or_start 450 | assert {:ok, [{call, response}]} = Spy.calls(spy) 451 | 452 | # Assert that the call is what we would expect for a server-side lookup_or_start 453 | assert {:start, :test_id, [:test]} == call 454 | 455 | # Assert that the response is the one returned from the call 456 | assert {:ok, pid} == response 457 | end 458 | end 459 | 460 | describe "stop/2" do 461 | setup [:start_registry] 462 | 463 | test "unknown id" do 464 | assert {:error, :not_found} = GenRegistry.stop(ExampleWorker, :unknown) 465 | end 466 | 467 | test "known id" do 468 | # Start a process 469 | assert {:ok, pid} = GenRegistry.lookup_or_start(ExampleWorker, :test_id) 470 | 471 | # Confirm that the process is alive 472 | assert Process.alive?(pid) 473 | 474 | # Confirm that the registry has 1 process 475 | assert 1 == GenRegistry.count(ExampleWorker) 476 | 477 | # Stop the process 478 | assert :ok = GenRegistry.stop(ExampleWorker, :test_id) 479 | 480 | # Confirm that the process is dead 481 | refute Process.alive?(pid) 482 | 483 | # Confirm that the registry has 0 processes 484 | assert 0 == GenRegistry.count(ExampleWorker) 485 | end 486 | end 487 | 488 | describe "count/1" do 489 | setup [:start_registry] 490 | 491 | test "empty registry" do 492 | assert 0 == GenRegistry.count(ExampleWorker) 493 | end 494 | 495 | test "populated registry" do 496 | for i <- 1..5 do 497 | assert {:ok, _} = GenRegistry.lookup_or_start(ExampleWorker, i) 498 | assert i == GenRegistry.count(ExampleWorker) 499 | end 500 | end 501 | 502 | test "decreases when process stopped" do 503 | # Confirm the initial count for the empty registry 504 | assert 0 == GenRegistry.count(ExampleWorker) 505 | 506 | # Start a process 507 | assert {:ok, _} = GenRegistry.lookup_or_start(ExampleWorker, :test_id) 508 | 509 | # Confirm that the count incremented for the new process 510 | assert 1 == GenRegistry.count(ExampleWorker) 511 | 512 | # Stop the process 513 | assert :ok = GenRegistry.stop(ExampleWorker, :test_id) 514 | 515 | # Confirm that the count decremented to account for the process stopping 516 | assert 0 == GenRegistry.count(ExampleWorker) 517 | end 518 | 519 | test "decreases when process exits" do 520 | # Confirm the initial count for the empty registry 521 | assert 0 == GenRegistry.count(ExampleWorker) 522 | 523 | # Start a process 524 | assert {:ok, pid} = GenRegistry.lookup_or_start(ExampleWorker, :test_id) 525 | 526 | # Confirm that the count incremented for the new process 527 | assert 1 == GenRegistry.count(ExampleWorker) 528 | 529 | # Force the process to exit 530 | GenServer.stop(pid) 531 | 532 | # Confirm that the count decremented to account for the process exiting 533 | assert wait_until(fn -> 534 | GenRegistry.count(ExampleWorker) == 0 535 | end) 536 | end 537 | end 538 | 539 | describe "reduce/3" do 540 | setup [:start_registry] 541 | 542 | def collect({id, pid}, acc) do 543 | [{id, pid} | acc] 544 | end 545 | 546 | test "empty registry, empty accumulator" do 547 | assert [] == GenRegistry.reduce(ExampleWorker, [], &collect/2) 548 | end 549 | 550 | test "empty registry, populated accumulator" do 551 | acc = [1, 2, 3] 552 | 553 | assert ^acc = GenRegistry.reduce(ExampleWorker, acc, &collect/2) 554 | end 555 | 556 | test "populated registry, empty accumulator" do 557 | expected = 558 | for i <- 1..5 do 559 | assert {:ok, pid} = GenRegistry.lookup_or_start(ExampleWorker, i) 560 | {i, pid} 561 | end 562 | 563 | # Note: Reduce doesn't guarantee ordering, the sort makes comparison simpler 564 | actual = 565 | ExampleWorker 566 | |> GenRegistry.reduce([], &collect/2) 567 | |> Enum.sort() 568 | 569 | assert expected == actual 570 | end 571 | 572 | test "populated registry, populated accumulator" do 573 | acc = [{1, nil}, {2, nil}, {3, nil}] 574 | 575 | spawned = 576 | for i <- 4..5 do 577 | assert {:ok, pid} = GenRegistry.lookup_or_start(ExampleWorker, i) 578 | {i, pid} 579 | end 580 | 581 | expected = acc ++ spawned 582 | 583 | actual = 584 | ExampleWorker 585 | |> GenRegistry.reduce(acc, &collect/2) 586 | |> Enum.sort() 587 | 588 | assert expected == actual 589 | end 590 | end 591 | 592 | describe "sample/1" do 593 | setup [:start_registry] 594 | 595 | test "empty registry returns nil" do 596 | refute GenRegistry.sample(ExampleWorker) 597 | end 598 | 599 | test "returns one of the entries in the registry" do 600 | candidates = 601 | for i <- 1..5 do 602 | assert {:ok, pid} = GenRegistry.lookup_or_start(ExampleWorker, i) 603 | {i, pid} 604 | end 605 | 606 | assert sample = GenRegistry.sample(ExampleWorker) 607 | 608 | assert sample in candidates 609 | end 610 | end 611 | 612 | describe "to_list/1" do 613 | setup [:start_registry] 614 | 615 | test "empty registry returns empty list" do 616 | assert [] == GenRegistry.to_list(ExampleWorker) 617 | end 618 | 619 | test "populated registry returns list of {id, pid} tuples" do 620 | expected = 621 | for i <- 1..5 do 622 | assert {:ok, pid} = GenRegistry.lookup_or_start(ExampleWorker, i) 623 | {i, pid} 624 | end 625 | 626 | actual = 627 | ExampleWorker 628 | |> GenRegistry.to_list() 629 | |> Enum.sort() 630 | 631 | assert expected == actual 632 | end 633 | end 634 | 635 | describe "registered process exit" do 636 | setup [:start_registry] 637 | 638 | test "count decrements" do 639 | # Start a process 640 | assert {:ok, pid} = GenRegistry.lookup_or_start(ExampleWorker, :test_id) 641 | 642 | # Confirm that the process is alive 643 | assert Process.alive?(pid) 644 | 645 | # Confirm that the registry has 1 process 646 | assert 1 == GenRegistry.count(ExampleWorker) 647 | 648 | # Force the process to exit 649 | Process.exit(pid, :kill) 650 | 651 | # Confirm that the process has exited 652 | refute Process.alive?(pid) 653 | 654 | # Wait for the registry to process the exit 655 | assert wait_until(fn -> 656 | GenRegistry.count(ExampleWorker) == 0 657 | end) 658 | 659 | # Confirm that the registry has 0 processes 660 | assert 0 == GenRegistry.count(ExampleWorker) 661 | end 662 | 663 | test "id is removed from the registry" do 664 | # Start a process 665 | assert {:ok, pid} = GenRegistry.lookup_or_start(ExampleWorker, :test_id) 666 | 667 | # Confirm that the process is alive 668 | assert Process.alive?(pid) 669 | 670 | # Confirm that the registry knows about the id 671 | assert {:ok, ^pid} = GenRegistry.lookup(ExampleWorker, :test_id) 672 | 673 | # Force the process to exit 674 | Process.exit(pid, :kill) 675 | 676 | # Confirm that the process has exited 677 | refute Process.alive?(pid) 678 | 679 | # Wait for the registry to process the exit 680 | assert wait_until(fn -> 681 | GenRegistry.count(ExampleWorker) == 0 682 | end) 683 | 684 | # Confirm that the registry no longer knows about the id 685 | assert {:error, :not_found} = GenRegistry.lookup(ExampleWorker, :test_id) 686 | end 687 | end 688 | 689 | describe "stopping a registry" do 690 | setup [:start_registry] 691 | 692 | test "exits all registered processes" do 693 | registry = Process.whereis(ExampleWorker) 694 | 695 | # Start some processes 696 | pids = 697 | for i <- 1..5 do 698 | assert {:ok, pid} = GenRegistry.lookup_or_start(ExampleWorker, i) 699 | pid 700 | end 701 | 702 | # Confirm that all processes are alive 703 | assert Enum.all?(pids, &Process.alive?/1) 704 | 705 | # Stop the registry 706 | assert :ok == stop_supervised(ExampleWorker) 707 | 708 | # Wait for the registry to die 709 | assert wait_until(fn -> 710 | not Process.alive?(registry) 711 | end) 712 | 713 | # Confirm that all process are dead 714 | refute Enum.any?(pids, &Process.alive?/1) 715 | end 716 | end 717 | 718 | describe "custom name" do 719 | test "multiple GenRegistries can be started for the same module with custom names" do 720 | {:ok, _} = 721 | start_supervised({GenRegistry, worker_module: ExampleWorker, name: ExampleWorker.A}) 722 | 723 | {:ok, _} = 724 | start_supervised({GenRegistry, worker_module: ExampleWorker, name: ExampleWorker.B}) 725 | 726 | assert {:error, :not_found} = GenRegistry.lookup(ExampleWorker.A, :test_id) 727 | 728 | assert {:ok, worker_a_pid} = 729 | GenRegistry.lookup_or_start(ExampleWorker.A, :test_id, [:test_value_a]) 730 | 731 | assert {:error, :not_found} = GenRegistry.lookup(ExampleWorker.B, :test_id) 732 | 733 | assert {:ok, worker_b_pid} = 734 | GenRegistry.lookup_or_start(ExampleWorker.B, :test_id, [:test_value_b]) 735 | 736 | assert worker_a_pid != worker_b_pid 737 | 738 | assert ExampleWorker.get(worker_a_pid) == :test_value_a 739 | assert ExampleWorker.get(worker_b_pid) == :test_value_b 740 | 741 | stop_supervised(ExampleWorker.A) 742 | stop_supervised(ExampleWorker.B) 743 | end 744 | end 745 | end 746 | --------------------------------------------------------------------------------