The Registry module
Navigation
Home Other GenServe functionsSupervisors introductionWhat is Registry?
From the official documentation
> A local, decentralized and scalable key-value process storage. It allows developers to lookup one or more processes with a given key.
Lets go through some important points about registries…
- The Registry in Elixir is a process store that stores key-value pairs, allowing us to register a process under a specific name.
- There are two types of Registries: unique and duplicate. A unique Registry only permits one process to be registered under a given name, while a duplicate Registry permits multiple processes to be registered under the same name.
- Each entry in the Registry is associated with the process that registered it. If the process crashes, the Registry automatically removes the keys associated with that process.
- The Registry compares keys using the match operation (===/2).
- Partitioning the Registry is possible, allowing for more scalable behavior in highly concurrent environments with thousands or millions of entries.
- The Registry uses ETS tables to store data under the hood.
- Registries can only be run locally and donot support distributed access.
Where to use Registry?
The most common use of Registry is to name process. The :via
is frequently used to specify the process name when using the Registry.
In addition to process naming, the Registry offers other useful features such as a dispatch mechanism that enables developers to implement custom logic for request initiation. With this dispatching mechanism, developers can build scalable and highly efficient systems, such as a local PubSub, by utilizing the dispatch/3
function.
Naming processes using Registry
The most common use of Registry is in naming processes.
First we start the Registry process
# We start a Registry process and name it "Registry.ProcessStore"
# Notice we use `keys: :unique` option which means every key in the Registry
# will point to a single process
{:ok, _} = Registry.start_link(keys: :unique, name: Registry.ProcessStore)
Now lets use this registry to name a GenServer
defmodule Stack do
use GenServer
# Callbacks
@impl true
def init(stack) do
{:ok, stack}
end
@impl true
def handle_cast({:push, e}, stack) do
{:noreply, [e | stack]}
end
# Other Callbacks ....
end
Now that we have a simple GenServer lets try to start 2 instances of this GenServer and name each of them using the Registry.
# Start the Stack GenServer and register it under the "Registry.ProcessStore"
# with a key named "stack_server_1"
GenServer.start_link(
Stack,
[],
name: {:via, Registry, {Registry.ProcessStore, "stack_server_1"}}
)
# Start another instance of the Stack server with the name "stack_server_2"
# Notice how we also store an optional value associated with this process `:second_stack`
GenServer.start_link(
Stack,
[],
name: {:via, Registry, {Registry.ProcessStore, "stack_server_2", :second_stack}}
)
When we register a process under a Registry, we have the option to store an associated metadata with that entry. In the second example mentioned above, we not only registered an instance of our Stack GenServer process under the registry but also stored the value :second_stack
along with its corresponding entry.
Now lets call our Stack GenServer using its Registered name, we can use the lookup/2
function that returns a list like [{pid(), value()}]
. For Registries that allow duplicate entries a lookup can return multiple entries in this list.
# Since we use an unique Registry, its guaranteed we will only get atmost
# one process under the name "stack_server"
[{stack_server_one_pid, nil}] = Registry.lookup(Registry.ProcessStore, "stack_server_1")
GenServer.cast(stack_server_one_pid, {:push, "stack1"})
[{stack_server_two_pid, value}] = Registry.lookup(Registry.ProcessStore, "stack_server_2")
IO.inspect(value, label: "Stack server 2 value")
GenServer.cast(stack_server_two_pid, {:push, "stack2"})
IO.inspect(:sys.get_state(stack_server_one_pid), label: "Stack Server1 state")
IO.inspect(:sys.get_state(stack_server_two_pid), label: "Stack Server2 state")
Let us now explore how a Registry operates when we permit the storage of duplicate entries.
When utilizing duplicate registries, it is not possible to use the :via option. To illustrate how duplicate registries function, let us attempt to register the current process twice using the register/3
function.
{:ok, _} = Registry.start_link(keys: :duplicate, name: Registry.DupProcessStore)
{:ok, _} = Registry.register(Registry.DupProcessStore, "async_city", :hello)
{:ok, _} = Registry.register(Registry.DupProcessStore, "async_city", :world)
Registry.lookup(Registry.DupProcessStore, "async_city")
Observe how the invocation of Registry.lookup/2
resulted in a list containing 2 tuples, each representing a process along with its associated metadata. These two processes were registered under the identical name, “async_city”.
Dispatching using Registry
Dispatching allows us to fetch all entries for all processes registered under a given key. We pass a callback function which would receive the list of {pid, value}
for every entry registered under the given key.
It is worth noting that dispatching takes place in the process that initiates the dispatch/3
call, either serially or concurrently in the case of multiple partitions.
To better understand the concept of dispatching, let us take a look at an example.
# Start a Registry which allows duplicates
{:ok, _} = Registry.start_link(keys: :duplicate, name: Registry.Numbers)
# Register the current process 3 times under the same key "odd"
# Save a value along with registration that is 1, "3" and fn -> 5
{:ok, _} = Registry.register(Registry.Numbers, "odd", 1)
{:ok, _} = Registry.register(Registry.Numbers, "odd", "3")
{:ok, _} = Registry.register(Registry.Numbers, "odd", fn -> 5 end)
# Register the current process 3 times under another key "even"
{:ok, _} = Registry.register(Registry.Numbers, "even", 2)
{:ok, _} = Registry.register(Registry.Numbers, "even", "4")
{:ok, _} = Registry.register(Registry.Numbers, "even", fn -> 6 end)
# Dispatching on processes registered under the key "odd"
Registry.dispatch(Registry.Numbers, "odd", fn entries ->
for {_pid, num} <- entries do
cond do
is_number(num) -> num
is_binary(num) -> String.to_integer(num)
is_function(num) -> num.()
end
|> IO.inspect(label: "ODD")
end
end)
# Dispatching on processes registered under the key "even"
Registry.dispatch(Registry.Numbers, "even", fn entries ->
for {_pid, num} <- entries do
cond do
is_number(num) -> num
is_binary(num) -> String.to_integer(num)
is_function(num) -> num.()
end
|> IO.inspect(label: "EVEN")
end
end)
Building a pubsub system with Registry
We can also use this dispatch/3
function to implement a local, non-distributed PubSub.
This works by registering multiple processes under a given key which acts like a pubsub topic.
We can then send a message to all processes registered under a key to emulate a pubsub broadcast. Here we also set the number of partitions to the number of schedulers online, which will make the registry more performant on highly concurrent environments.
Lets see this in action.
{:ok, _} =
Registry.start_link(
keys: :duplicate,
name: Registry.ChatPubSub,
# The number of schedulers available in the VM
partitions: System.schedulers_online()
)
# Register the current process under the "Registry.ChatPubSub" registery with a key "chat_room:1"
{:ok, _} = Registry.register(Registry.ChatPubSub, "chat_room:1", [])
# Dispatching by looking up all process registered with the key "chat_room:1" in the
# "Registry.ChatPubSub" registry and then sending them a message.
Registry.dispatch(Registry.ChatPubSub, "chat_room:1", fn entries ->
for {pid, _} <- entries, do: send(pid, {:broadcast, "hello world"})
end)
# Receive any broadcasted messages
receive do
{:broadcast, message} -> IO.inspect(message, label: "Received broadcast")
end
By using this approach, we can register multiple processes under a single key within a Registry and subsequently dispatch messages to all the processes associated with that key.
Other registry functions and match specs
Apart from the register/3
and lookup/2
functions, the Registry module has several other useful functions which allows us to find and manipulate data inside the Registry. Most of these functions are straightforward to understand.
However, its worth noting that some functions use match specs to find matching entries from the Registry let us look at some examples to understand how match specs work.
From the official documentation
> A match spec is a pattern that must be an atom or a tuple that will match the structure of the value stored in the registry. The atom :_
can be used to ignore a given value or tuple element, while the atom :"$1"
can be used to temporarily assign part of pattern to a variable for a subsequent comparison.
> Optionally, it is possible to pass a list of guard conditions for more precise matching. Each guard is a tuple, which describes checks that should be passed by assigned part of pattern. For example the $1 > 1
guard condition would be expressed as the {:>, :"$1", 1}
tuple. Please note that guard conditions will work only for assigned variables like :”$1”, :”$2”, and so forth.
Lets consider the match/4
functions in the Registry module that returns entries from the Registry that matches the match spec passed.
Registry.start_link(keys: :duplicate, name: Registry.MatchSpec)
# Register the current process multiple times with different values under the key "my_key"
{:ok, _} = Registry.register(Registry.MatchSpec, "my_key", 1)
{:ok, _} = Registry.register(Registry.MatchSpec, "my_key", "one")
{:ok, _} = Registry.register(Registry.MatchSpec, "my_key", {1, 2})
{:ok, _} = Registry.register(Registry.MatchSpec, "my_key", {2, 1})
{:ok, _} = Registry.register(Registry.MatchSpec, "my_key", {2, 2})
# Use different match specs to find matching entries from the Registry under the key "my_key"
Registry.match(Registry.MatchSpec, "my_key", 1)
|> IO.inspect(label: "* match spec: 1 returned")
Registry.match(Registry.MatchSpec, "my_key", :_)
|> IO.inspect(label: "* match spec: :_ returned")
Registry.match(Registry.MatchSpec, "my_key", {2, :_})
|> IO.inspect(label: "* match spec: {2, :_} returned")
Registry.match(Registry.MatchSpec, "my_key", {:"$1", :"$1"})
|> IO.inspect(label: ~s(* match spec: {:"$1", :"$1"} returned))
# Also using guards along with match specs
Registry.match(Registry.MatchSpec, "my_key", {:"$1", :"$2"}, [{:>, :"$1", :"$2"}])
|> IO.inspect(label: ~s(* match spec: {:"$1", :"$2"} with guard [{:>, :"$1", :"$2"}] returned))
Registry.match(Registry.MatchSpec, "my_key", :"$1", [{:is_binary, :"$1"}])
|> IO.inspect(label: ~s(* match spec: :"$1" with guard [{:is_binary, :"$1"}] returned"))
Other functions like count_match/4
, select/2
, etc in the Registry module also use match specs for filtering entries in the Registry.
Resources
- The official Registry documentition: https://hexdocs.pm/elixir/1.14.4/Registry.html#content
- Guards in elixir: https://hexdocs.pm/elixir/1.14/patterns-and-guards.html#guards