Repository: commanded/commanded
Pull request #344 (2020-02-18 10:25:04)

Global registry using Erlang's `:global` module
Use distributed Erlang's [`:global`](http://erlang.org/doc/man/global.html) module as a distributed process registry. This supports multi node distributed Erlang deployment.


Patch:
diff --git a/.tool-versions b/.tool-versions
new file mode 100644
index 0000000..faafbd4
--- /dev/null
+++ b/.tool-versions
@@ -0,0 +1,1 @@
+elixir 1.9.4-otp-22
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0f9c8fd..52e377c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Changelog
 
+## Next release
+
+### Enhancements
+
+- Global registry using Erlang's `:global` module ([#344](https://github.com/commanded/commanded/pull/344)).
+
 ## v1.0.0
 
 ### Breaking changes
diff --git a/guides/Deployment.md b/guides/Deployment.md
index 528f6f6..d16f604 100644
--- a/guides/Deployment.md
+++ b/guides/Deployment.md
@@ -6,9 +6,37 @@ Commanded supports running on a single node, or multiple nodes run as either a [
 
 Running your app using Commanded on a single node requires no configuration as local is the default setting.
 
-## Multi node cluster deployment
+## Multi node distributed Erlang deployment
 
-To support deployment to a cluster of nodes you must use the [Commanded Swarm registry](https://github.com/commanded/commanded-swarm-registry) library and [Phoenix's distributed pub/sub and presence platform](https://hex.pm/packages/phoenix_pubsub) to allow process distribution and communication amongst all nodes in the cluster.
+To support deployment to a cluster of nodes and distributed Erlang you must configure:
+
+1. A registry which supports distributed Erlang. The `:global` registry provided by Commanded or the [Commanded Swarm registry](https://github.com/commanded/commanded-swarm-registry) library.
+2. [Phoenix's distributed pub/sub and presence platform](https://hex.pm/packages/phoenix_pubsub) to allow process distribution and communication amongst all nodes in the cluster.
+
+### `:global` registry
+
+Use Erlang's [`:global`](http://erlang.org/doc/man/global.html) name registration facility with distributed Erlang. The global name server starts automatically when a node is started. The registered names are stored in replica global name tables on every node. Thus, the translation of a name to a pid is fast, as it is always done locally.
+
+Define the `:global` registry for your application:
+
+```elixir
+defmodule MyApp.Application do
+  use Commanded.Application,
+    otp_app: :my_app,
+    event_store: [
+      adapter: Commanded.EventStore.Adapters.EventStore,
+      event_store: MyApp.EventStore
+    ],
+    registry: :global
+end
+```
+
+Or configure your application to use the `:global` registry in config:
+
+```elixir
+# config/config.exs
+config :my_app, MyApp.Application, registry: :global
+```
 
 ### Commanded Swarm registry
 
@@ -16,9 +44,7 @@ Add `commanded_swarm_registry` to your list of dependencies in `mix.exs`:
 
 ```elixir
 def deps do
-  [
-    {:commanded_swarm_registry, "~> 0.1"}
-  ]
+  [{:commanded_swarm_registry, "~> 0.1"}]
 end
 ```
 
@@ -43,14 +69,14 @@ First, add it as a dependency to your project's `mix.exs` file:
 
 ```elixir
 defp deps do
-  [{:phoenix_pubsub, "~> 1.0"}]
+  [{:phoenix_pubsub, "~> 1.1"}]
 end
 ```
 
 Fetch mix deps and configure the pubsub settings for your application in the environment config file:
 
 ```elixir
-# `config/config.exs`
+# config/config.exs
 config :my_app, MyApp.Application,
   pubsub: [
     phoenix_pubsub: [
@@ -62,9 +88,9 @@ config :my_app, MyApp.Application,
 
 The PG2 adapter is preferable for cluster deployment since it is provided by Erlang and requires no further dependencies.
 
-## Multi node, but not clustered deployment
+## Multi node, but not distributed Erlang deployment
 
-Running multiple nodes, but choosing not to connect the nodes together to form a cluster, requires that you use the local registry and Phoenix's pub/sub library with its Redis adapter.
+Running multiple nodes, but choosing not to connect the nodes together to form a distributed Erlang cluster, requires that you use the local registry and Phoenix's pub/sub library with its Redis adapter.
 
 You must install and use Phoenix's pub/sub library, [as described above](#phoenix-pub-sub).
 
diff --git a/lib/commanded/registration.ex b/lib/commanded/registration.ex
index 0499ab0..eecbd3d 100644
--- a/lib/commanded/registration.ex
+++ b/lib/commanded/registration.ex
@@ -54,6 +54,9 @@ defmodule Commanded.Registration do
       :local ->
         {Commanded.Registration.LocalRegistry, []}
 
+      :global ->
+        {Commanded.Registration.GlobalRegistry, []}
+
       adapter when is_atom(adapter) ->
         {adapter, []}
 
diff --git a/lib/commanded/registration/global_registry.ex b/lib/commanded/registration/global_registry.ex
new file mode 100644
index 0000000..78c6f67
--- /dev/null
+++ b/lib/commanded/registration/global_registry.ex
@@ -0,0 +1,121 @@
+defmodule Commanded.Registration.GlobalRegistry do
+  @moduledoc """
+  Distributed process registration using Erlangs `:global` registry[1].
+
+  [1] http://erlang.org/doc/man/global.html
+  """
+
+  @behaviour Commanded.Registration.Adapter
+
+  @doc """
+  Return an optional supervisor spec for the registry
+  """
+  @impl Commanded.Registration.Adapter
+  def child_spec(application, _config) do
+    {:ok, [], %{application: application}}
+  end
+
+  @doc """
+  Starts a supervisor.
+  """
+  @impl Commanded.Registration.Adapter
+  def supervisor_child_spec(_adapter_meta, module, arg) do
+    spec = %{
+      id: module,
+      start: {module, :start_link, [arg]},
+      type: :supervisor
+    }
+
+    Supervisor.child_spec(spec, [])
+  end
+
+  @doc """
+  Starts a uniquely named child process of a supervisor using the given module
+  and args.
+
+  Registers the pid with the given name.
+  """
+  @impl Commanded.Registration.Adapter
+  def start_child(adapter_meta, name, supervisor, child_spec) do
+    via_name = via_tuple(adapter_meta, name)
+
+    child_spec =
+      case child_spec do
+        module when is_atom(module) ->
+          {module, name: via_name}
+
+        {module, args} when is_atom(module) and is_list(args) ->
+          {module, Keyword.put(args, :name, via_name)}
+      end
+
+    case DynamicSupervisor.start_child(supervisor, child_spec) do
+      {:error, {:already_started, pid}} -> {:ok, pid}
+      reply -> reply
+    end
+  end
+
+  @doc """
+  Starts a uniquely named `GenServer` process for the given module and args.
+
+  Registers the pid with the given name.
+  """
+  @impl Commanded.Registration.Adapter
+  def start_link(adapter_meta, name, module, args) do
+    via_name = via_tuple(adapter_meta, name)
+
+    case GenServer.start_link(module, args, name: via_name) do
+      {:error, {:already_started, pid}} -> {:ok, pid}
+      reply -> reply
+    end
+  end
+
+  @doc """
+  Get the pid of a registered name.
+
+  Returns `:undefined` if the name is unregistered.
+  """
+  @impl Commanded.Registration.Adapter
+  def whereis_name(adapter_meta, name) do
+    global_name = global_name(adapter_meta, name)
+
+    :global.whereis_name(global_name)
+  end
+
+  @doc """
+  Return a `:via` tuple to route a message to a process by its registered name
+  """
+  @impl Commanded.Registration.Adapter
+  def via_tuple(adapter_meta, name) do
+    global_name = global_name(adapter_meta, name)
+
+    {:via, :global, global_name}
+  end
+
+  @doc false
+  def handle_call(_request, _from, _state) do
+    raise "attempted to call GenServer #{inspect(proc())} but no handle_call/3 clause was provided"
+  end
+
+  @doc false
+  def handle_cast(_request, _state) do
+    raise "attempted to cast GenServer #{inspect(proc())} but no handle_cast/2 clause was provided"
+  end
+
+  @doc false
+  def handle_info(_msg, state) do
+    {:noreply, state}
+  end
+
+  defp proc do
+    case Process.info(self(), :registered_name) do
+      {_, []} -> self()
+      {_, name} -> name
+    end
+  end
+
+  defp global_name(adapter_meta, name) do
+    application = Map.fetch!(adapter_meta, :application)
+
+    {application, name}
+  end
+end
diff --git a/mix.lock b/mix.lock
index fca76ff..be1fcd0 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,16 +1,16 @@
 %{
-  "benchfella": {:hex, :benchfella, "0.3.5", "b2122c234117b3f91ed7b43b6e915e19e1ab216971154acd0a80ce0e9b8c05f5", [:mix], [], "hexpm"},
-  "dialyxir": {:hex, :dialyxir, "1.0.0-rc.7", "6287f8f2cb45df8584317a4be1075b8c9b8a69de8eeb82b4d9e6c761cf2664cd", [:mix], [{:erlex, ">= 0.2.5", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"},
-  "earmark": {:hex, :earmark, "1.4.2", "3aa0bd23bc4c61cf2f1e5d752d1bb470560a6f8539974f767a38923bb20e1d7f", [:mix], [], "hexpm"},
-  "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm"},
-  "erlex": {:hex, :erlex, "0.2.5", "e51132f2f472e13d606d808f0574508eeea2030d487fc002b46ad97e738b0510", [:mix], [], "hexpm"},
-  "ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
-  "file_system": {:hex, :file_system, "0.2.7", "e6f7f155970975789f26e77b8b8d8ab084c59844d8ecfaf58cbda31c494d14aa", [:mix], [], "hexpm"},
-  "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
-  "makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
-  "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
-  "mix_test_watch": {:hex, :mix_test_watch, "1.0.2", "34900184cbbbc6b6ed616ed3a8ea9b791f9fd2088419352a6d3200525637f785", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm"},
-  "mox": {:hex, :mox, "0.5.1", "f86bb36026aac1e6f924a4b6d024b05e9adbed5c63e8daa069bd66fb3292165b", [:mix], [], "hexpm"},
-  "nimble_parsec": {:hex, :nimble_parsec, "0.5.2", "1d71150d5293d703a9c38d4329da57d3935faed2031d64bc19e77b654ef2d177", [:mix], [], "hexpm"},
-  "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm"},
+  "benchfella": {:hex, :benchfella, "0.3.5", "b2122c234117b3f91ed7b43b6e915e19e1ab216971154acd0a80ce0e9b8c05f5", [:mix], [], "hexpm", "23f27cbc482cbac03fc8926441eb60a5e111759c17642bac005c3225f5eb809d"},
+  "dialyxir": {:hex, :dialyxir, "1.0.0-rc.7", "6287f8f2cb45df8584317a4be1075b8c9b8a69de8eeb82b4d9e6c761cf2664cd", [:mix], [{:erlex, ">= 0.2.5", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "506294d6c543e4e5282d4852aead19ace8a35bedeb043f9256a06a6336827122"},
+  "earmark": {:hex, :earmark, "1.4.2", "3aa0bd23bc4c61cf2f1e5d752d1bb470560a6f8539974f767a38923bb20e1d7f", [:mix], [], "hexpm", "5e8806285d8a3a8999bd38e4a73c58d28534c856bc38c44818e5ba85bbda16fb"},
+  "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"},
+  "erlex": {:hex, :erlex, "0.2.5", "e51132f2f472e13d606d808f0574508eeea2030d487fc002b46ad97e738b0510", [:mix], [], "hexpm", "756d3e19b056339af674b715fdd752c5dac468cf9d0e2d1a03abf4574e99fbf8"},
+  "ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f1155337ae17ff7a1255217b4c1ceefcd1860b7ceb1a1874031e7a861b052e39"},
+  "file_system": {:hex, :file_system, "0.2.7", "e6f7f155970975789f26e77b8b8d8ab084c59844d8ecfaf58cbda31c494d14aa", [:mix], [], "hexpm", "b4cfa2d69c7f0b18fd06db222b2398abeef743a72504e6bd7df9c52f171b047f"},
+  "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fdf843bca858203ae1de16da2ee206f53416bbda5dc8c9e78f43243de4bc3afe"},
+  "makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "a10c6eb62cca416019663129699769f0c2ccf39428b3bb3c0cb38c718a0c186d"},
+  "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"},
+  "mix_test_watch": {:hex, :mix_test_watch, "1.0.2", "34900184cbbbc6b6ed616ed3a8ea9b791f9fd2088419352a6d3200525637f785", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "47ac558d8b06f684773972c6d04fcc15590abdb97aeb7666da19fcbfdc441a07"},
+  "mox": {:hex, :mox, "0.5.1", "f86bb36026aac1e6f924a4b6d024b05e9adbed5c63e8daa069bd66fb3292165b", [:mix], [], "hexpm", "052346cf322311c49a0f22789f3698eea030eec09b8c47367f0686ef2634ae14"},
+  "nimble_parsec": {:hex, :nimble_parsec, "0.5.2", "1d71150d5293d703a9c38d4329da57d3935faed2031d64bc19e77b654ef2d177", [:mix], [], "hexpm", "51aa192e0941313c394956718bdb1e59325874f88f45871cff90345b97f60bba"},
+  "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm", "1f13f9f0f3e769a667a6b6828d29dec37497a082d195cc52dbef401a9b69bf38"},
 }


Test patch:
diff --git a/test/registration/global_registry_test.exs b/test/registration/global_registry_test.exs
new file mode 100644
index 0000000..6d9081d
--- /dev/null
+++ b/test/registration/global_registry_test.exs
@@ -0,0 +1,14 @@
+defmodule Commanded.GlobalRegistryTest do
+  alias Commanded.Registration.GlobalRegistry
+  alias Commanded.RegistrationTestCase
+
+  use RegistrationTestCase, registry: GlobalRegistry
+
+  setup do
+    {:ok, child_spec, registry_meta} = GlobalRegistry.child_spec(GlobalRegistry, [])
+
+    for child <- child_spec, do: start_supervised!(child)
+
+    [registry_meta: registry_meta]
+  end
+end
diff --git a/test/registration/local_registry_test.exs b/test/registration/local_registry_test.exs
index 72141c5..9a53785 100644
--- a/test/registration/local_registry_test.exs
+++ b/test/registration/local_registry_test.exs
@@ -1,7 +1,14 @@
 defmodule Commanded.LocalRegistryTest do
-  alias Commanded.DefaultApp
-  alias Commanded.RegistrationTestCase
   alias Commanded.Registration.LocalRegistry
+  alias Commanded.RegistrationTestCase
+
+  use RegistrationTestCase, registry: LocalRegistry
+
+  setup do
+    {:ok, child_spec, registry_meta} = LocalRegistry.child_spec(LocalRegistry, [])
+
+    for child <- child_spec, do: start_supervised!(child)
 
-  use RegistrationTestCase, application: DefaultApp, registry: LocalRegistry
+    [registry_meta: registry_meta]
+  end
 end
diff --git a/test/registration/support/registered_supervisor.ex b/test/registration/support/registered_supervisor.ex
index 5afccbf..7e72961 100644
--- a/test/registration/support/registered_supervisor.ex
+++ b/test/registration/support/registered_supervisor.ex
@@ -1,15 +1,14 @@
 defmodule Commanded.Registration.RegisteredSupervisor do
   use DynamicSupervisor
 
-  alias Commanded.Registration
   alias Commanded.Registration.RegisteredServer
 
   def start_link(arg) do
     DynamicSupervisor.start_link(__MODULE__, arg, name: __MODULE__)
   end
 
-  def start_child(application, name) do
-    Registration.start_child(application, name, __MODULE__, {RegisteredServer, []})
+  def start_child(registry, registry_meta, name) do
+    registry.start_child(registry_meta, name, __MODULE__, {RegisteredServer, []})
   end
 
   def init(_arg) do
diff --git a/test/registration/support/registration_test_case.ex b/test/registration/support/registration_test_case.ex
index d50fb2b..1397725 100644
--- a/test/registration/support/registration_test_case.ex
+++ b/test/registration/support/registration_test_case.ex
@@ -2,60 +2,78 @@ defmodule Commanded.RegistrationTestCase do
   import Commanded.SharedTestCase
 
   define_tests do
-    alias Commanded.Registration
     alias Commanded.Registration.{RegisteredServer, RegisteredSupervisor}
 
-    setup %{application: application} do
-      start_supervised!(application)
+    setup do
       supervisor = start_supervised!(RegisteredSupervisor)
 
       [supervisor: supervisor]
     end
 
     describe "`start_child/3`" do
-      test "should return child process PID on success", %{application: application} do
-        assert {:ok, _pid} = RegisteredSupervisor.start_child(application, "child")
+      test "should return child process PID on success", %{
+        registry: registry,
+        registry_meta: registry_meta
+      } do
+        assert {:ok, pid} = RegisteredSupervisor.start_child(registry, registry_meta, "child")
+        assert is_pid(pid)
       end
 
       test "should return existing child process when already started", %{
-        application: application
+        registry: registry,
+        registry_meta: registry_meta
       } do
-        assert {:ok, pid} = RegisteredSupervisor.start_child(application, "child")
-        assert {:ok, ^pid} = RegisteredSupervisor.start_child(application, "child")
+        assert {:ok, pid} = RegisteredSupervisor.start_child(registry, registry_meta, "child")
+        assert {:ok, ^pid} = RegisteredSupervisor.start_child(registry, registry_meta, "child")
       end
     end
 
     describe "`start_link/3`" do
-      test "should return process PID on success", %{application: application} do
-        assert {:ok, pid} = start_link(application, "registered")
+      test "should return process PID on success", %{
+        registry: registry,
+        registry_meta: registry_meta
+      } do
+        assert {:ok, pid} = start_link(registry, registry_meta, "registered")
         assert is_pid(pid)
       end
 
-      test "should return existing process when already started", %{application: application} do
-        assert {:ok, pid} = start_link(application, "registered")
-        assert {:ok, ^pid} = start_link(application, "registered")
+      test "should return existing process when already started", %{
+        registry: registry,
+        registry_meta: registry_meta
+      } do
+        assert {:ok, pid} = start_link(registry, registry_meta, "registered")
+        assert {:ok, ^pid} = start_link(registry, registry_meta, "registered")
       end
     end
 
     describe "`whereis_name/1`" do
-      test "should return `:undefined` when not registered", %{application: application} do
-        assert Registration.whereis_name(application, "notregistered") == :undefined
+      test "should return `:undefined` when not registered", %{
+        registry: registry,
+        registry_meta: registry_meta
+      } do
+        assert registry.whereis_name(registry_meta, "notregistered") == :undefined
       end
 
-      test "should return `PID` when child registered", %{application: application} do
-        assert {:ok, pid} = RegisteredSupervisor.start_child(application, "child")
-        assert Registration.whereis_name(application, "child") == pid
+      test "should return `PID` when child registered", %{
+        registry: registry,
+        registry_meta: registry_meta
+      } do
+        assert {:ok, pid} = RegisteredSupervisor.start_child(registry, registry_meta, "child")
+        assert registry.whereis_name(registry_meta, "child") == pid
       end
 
-      test "should return `PID` when process registered", %{application: application} do
-        assert {:ok, pid} = start_link(application, "registered")
-        assert Registration.whereis_name(application, "registered") == pid
+      test "should return `PID` when process registered", %{
+        registry: registry,
+        registry_meta: registry_meta
+      } do
+        assert {:ok, pid} = start_link(registry, registry_meta, "registered")
+        assert registry.whereis_name(registry_meta, "registered") == pid
       end
     end
 
     describe "`supervisor_child_spec/2`" do
-      test "should return a valid child_spec", %{application: application} do
-        assert Registration.supervisor_child_spec(application, RegisteredSupervisor, "child") ==
+      test "should return a valid child_spec", %{registry: registry, registry_meta: registry_meta} do
+        assert registry.supervisor_child_spec(registry_meta, RegisteredSupervisor, "child") ==
                  %{
                    id: Commanded.Registration.RegisteredSupervisor,
                    start: {Commanded.Registration.RegisteredSupervisor, :start_link, ["child"]},
@@ -64,8 +82,8 @@ defmodule Commanded.RegistrationTestCase do
       end
     end
 
-    defp start_link(application, name) do
-      Registration.start_link(application, name, RegisteredServer, [])
+    defp start_link(registry, registry_meta, name) do
+      registry.start_link(registry_meta, name, RegisteredServer, [])
     end
   end
 end