defmodule RaRegistry do @moduledoc """ A distributed registry for Elixir GenServers using Ra (RabbitMQ's Raft implementation). RaRegistry provides similar functionality to Elixir's built-in Registry module, but with distributed consensus via Ra, making it suitable for distributed applications across multiple nodes. It supports both :unique and :duplicate registration modes. ## Usage ```elixir # Register a process with a unique key RaRegistry.register(:my_registry, "unique_key", :some_value) # Look up a process by key RaRegistry.lookup(:my_registry, "unique_key") # Register with a duplicate key RaRegistry.register(:my_duplicate_registry, "shared_key", :value1) RaRegistry.register(:my_duplicate_registry, "shared_key", :value2) # Look up all processes with the duplicate key RaRegistry.lookup(:my_duplicate_registry, "shared_key") ``` ## Via Registration You can also use RaRegistry as a process registry with GenServer and other OTP processes by using the `:via` tuple: ```elixir # Start a GenServer with RaRegistry name = {:via, RaRegistry, {MyRegistry, "my_server"}} {:ok, pid} = GenServer.start_link(MyServer, arg, name: name) # Call the server using the same via tuple GenServer.call(name, :some_request) ``` """ @doc """ Starts a new registry with the given name and options. ## Options * `:keys` - The kind of keys in this registry. Can be either `:unique` or `:duplicate` (required). * `:name` - A local or global name for the registry (required). ## Examples RaRegistry.start_link(keys: :unique, name: MyRegistry) RaRegistry.start_link(keys: :duplicate, name: {MyRegistry, self()}) """ def start_link(opts) do name = Keyword.fetch!(opts, :name) # Start a supervision tree with the manager children = [ {RaRegistry.Manager, opts} ] Supervisor.start_link(children, strategy: :one_for_one, name: supervisor_name(name)) end @doc """ See `start_link/2` for options. """ def child_spec(options) when is_list(options) do id = Keyword.get(options, :name, RaRegistry) %{ id: id, start: {RaRegistry, :start_link, [options]}, type: :supervisor } end @doc """ Returns the registry key type, either :unique or :duplicate. """ def keys(name) do GenServer.call(name, :get_keys) end @doc """ Registers the given process under the given key in the registry. If no pid is provided, registers the current process. ## Examples RaRegistry.register(MyRegistry, "key", :value) RaRegistry.register(MyRegistry, "key", :value, other_pid) """ def register(name, key, value \\ nil, pid \\ nil) do pid = pid || self() GenServer.call(name, {:register, key, pid, value}) end @doc """ Unregisters the current process for the given key in the registry. ## Examples RaRegistry.unregister(MyRegistry, "key") """ def unregister(name, key, pid \\ nil) do pid = pid || self() GenServer.call(name, {:unregister, key, pid}) end @doc """ Looks up the given key in the registry and returns the associated processes. ## Examples RaRegistry.lookup(MyRegistry, "key") """ def lookup(name, key) do GenServer.call(name, {:lookup, key}) |> Enum.filter(fn {pid, _value} when is_pid(pid) -> distributed_alive?(pid) _value -> true end) end @doc """ Returns the number of processes registered under the given key. ## Examples RaRegistry.count(MyRegistry, "key") """ def count(name, key) do GenServer.call(name, {:count, key}) end @doc """ Updates the value associated with the key for the current process. ## Examples RaRegistry.update_value(MyRegistry, "key", fn value -> value + 1 end) """ def update_value(name, key, callback) when is_function(callback, 1) do GenServer.call(name, {:update_value, key, self(), callback}) end # Private helpers defp supervisor_name(name) do :"#{name}.Supervisor" end @doc """ Implementation for the `:via` registration mechanism. Registers the current process under the given `key` in the registry `name`. Returns `:yes` if registration succeeds, `:no` otherwise. """ @spec register_name({registry :: term, key :: term}, pid) :: :yes | :no def register_name({registry, key}, pid) do # For compatibility with GenServer :via # We must use the provided pid, not self() case register(registry, key, nil, pid) do :ok -> :yes _ -> :no end end @doc """ Implementation for the `:via` registration mechanism. Returns the pid associated with the given `key` in the registry `name`. Returns `pid` if successful, `:undefined` otherwise. """ @spec whereis_name({registry :: term, key :: term}) :: pid | :undefined def whereis_name({registry, key}) do case lookup(registry, key) do [{pid, _}] -> pid _ -> :undefined end end @doc """ Implementation for the `:via` registration mechanism. Unregisters the given `key` from the registry `name`. """ @spec unregister_name({registry :: term, key :: term}) :: :ok def unregister_name({registry, key}) do unregister(registry, key) :ok end @doc """ Implementation for the `:via` registration mechanism. Sends a message to the process registered under the given `key` in `registry`. """ @spec send({registry :: term, key :: term}, message :: term) :: pid def send({registry, key}, message) do case whereis_name({registry, key}) do :undefined -> raise "No process registered with key #{inspect(key)} in registry #{inspect(registry)}" pid -> Kernel.send(pid, message) pid end end defp distributed_alive?(pid) when is_pid(pid) do owner_node = node(pid) self = Node.self() if owner_node == self do Process.alive?(pid) else :erpc.call(owner_node, Process, :alive?, [pid]) end rescue _ -> false end defp distributed_alive?(_pid), do: false end