Current section
Files
Jump to
Current section
Files
lib/exreg.ex
defmodule ExReg do
@moduledoc """
This module defines the API for a simple and distributed process name
registry, using `:pg2`. The following features are supported:
- Accepts any term as process names.
- Works distributedly.
- Supports registering processes with the same name as long as they are in
different nodes.
```
iex>pid = self()
#PID<0.42.0>
iex> ExReg.register_name(:foo, pid)
:yes
iex> ExReg.whereis_name(:foo)
#PID<0.42.0>
iex> ExReg.send(:foo, "bar")
iex> flush()
"bar"
:ok
iex> ExReg.unregister_name(:foo)
:ok
iex> ExReg.send(:foo, "hey")
** (ArgumentError) Cannot send "hey" to :foo
```
"""
@typedoc """
Location.
"""
@type location :: :local | :global
@typedoc """
Process name.
"""
@type process_name :: term() | {location(), term()}
############
# Public API
@doc """
Generates a `:via` tuple for a global `name`.
"""
@spec global(term()) :: {:via, __MODULE__, process_name()}
def global(name), do: via(name, :global)
@doc """
Generates a `:via` tuple for a local `name`.
"""
@spec local(term()) :: {:via, __MODULE__, process_name()}
def local(name), do: via(name, :local)
@doc """
Registers `name` as an alias for a local `pid` globally.
"""
@spec register_name(process_name(), pid()) :: :yes | :no
def register_name(name, pid) do
case get_local_pid(name) do
{:error, {:no_such_group, internal_name}} ->
:pg2.create(internal_name)
:pg2.join(internal_name, pid)
:yes
{:error, {:no_process, internal_name}} ->
:pg2.join(internal_name, pid)
:yes
^pid ->
:yes
_ ->
:no
end
end
@doc """
Unregisters a `name`.
"""
@spec unregister_name(process_name()) :: :ok
def unregister_name(name) do
pid = self()
internal_name = get_internal_name(name)
case :pg2.get_members(internal_name) do
[] ->
:pg2.delete(internal_name)
[^pid] ->
:pg2.delete(internal_name)
_ ->
:ok
end
end
@doc """
Searches for the PID associated with the `name`.
"""
@spec whereis_name(process_name()) :: pid() | :undefined
def whereis_name(name)
def whereis_name({:global, _} = name) do
case get_closest_pid(name) do
pid when is_pid(pid) -> pid
_ -> :undefined
end
end
def whereis_name({:local, _} = name) do
case get_local_pid(name) do
pid when is_pid(pid) -> pid
_ -> :undefined
end
end
def whereis_name(name) do
whereis_name({:global, name})
end
@doc """
Sends a `message` to the PID associated with `name`.
"""
@spec send(process_name(), term()) :: pid() | no_return()
def send(name, message) do
case whereis_name(name) do
pid when is_pid(pid) ->
Kernel.send(pid, message)
pid
:undefined ->
raise ArgumentError,
message: "Cannot send #{inspect(message)} to #{inspect(name)}"
end
end
#########
# Helpers
# Generates a `:via` tuple given a valid `name` and, optionally, a `location`
# (defaults to `:global`).
@spec via(term(), location()) :: {:via, __MODULE__, process_name()}
defp via(name, location)
defp via(name, location) when location in [:local, :global] do
{:via, __MODULE__, {location, name}}
end
# Gets the internal process name.
@spec get_internal_name(process_name()) :: {:"$exreg", term()}
defp get_internal_name(name)
defp get_internal_name({:local, name}) do
get_internal_name(name)
end
defp get_internal_name({:global, name}) do
get_internal_name(name)
end
defp get_internal_name(name) do
{:"$exreg", name}
end
# Gets the closest PID given a process `name`.
@spec get_closest_pid(process_name()) ::
pid()
| {:error, {:no_process, term()}}
| {:error, {:no_such_group, term()}}
defp get_closest_pid(name)
defp get_closest_pid(name) do
name
|> get_internal_name()
|> :pg2.get_closest_pid()
end
# Gets the local PID for the given process `name`.
@spec get_local_pid(process_name()) ::
pid()
| {:error, {:no_process, term()}}
| {:error, {:no_such_group, term()}}
defp get_local_pid(name)
defp get_local_pid(name) do
internal_name = get_internal_name(name)
case :pg2.get_local_members(internal_name) do
[pid | _] when is_pid(pid) ->
pid
[] ->
{:error, {:no_process, internal_name}}
error ->
error
end
end
end