Sagents.ProcessRegistry (Sagents v0.12.0)
Copy MarkdownAbstraction over process registry implementations.
Supports two backends:
:local— Elixir's built-inRegistry(single-node, zero extra deps):horde—Horde.Registry(distributed, requires the:hordedependency)
Configuration
# config/config.exs (or runtime.exs)
# Single-node (default — no config needed)
config :sagents, :distribution, :local
# Distributed cluster
config :sagents, :distribution, :hordeWhen :horde is selected, Horde.Registry is started with members: :auto
so it automatically discovers other nodes in the Erlang cluster.
Availability
A lookup can only be answered while the registry process is alive on this
node. Two normal situations leave it unavailable: the node has not finished
starting Sagents.Supervisor, and Sagents.Supervisor has shut down while
the BEAM drains during a rolling deploy. In that second window the node is
still reachable by a load balancer but can serve no agent request at all.
Neither backend reports this condition on its own. Horde.Registry.lookup/2
derives its ETS table name arithmetically and reads it unguarded, and Elixir's
Registry does the same through Registry.key_info!/1, so both raise
ArgumentError from inside :ets when the table's owning process is gone.
This module puts a guard in front of them so the condition is reported rather
than escaping as an :ets error:
available?/0reports it as a boolean.fetch/1returns{:error, :registry_unavailable}, distinct from{:error, :not_registered}.lookup/1,select/1,count/0andkeys/1cannot express it in their return values, so they raiseSagents.RegistryUnavailableError.
:registry_unavailable must never be collapsed into "not registered".
A caller that reads "nothing is running" responds by starting an agent, so
collapsing the two lets a draining node start a second agent for a
conversation that already has one elsewhere. Both would then hold and persist
state for the same conversation, with nothing reporting it.
See Sagents.ready?/0 and docs/deployment.md.
Summary
Functions
Whether this node's registry can currently answer lookups.
Returns the child spec for the configured registry backend.
Returns the count of all registered entries.
Raise Sagents.RegistryUnavailableError unless the registry can answer.
Look up a single process by key, without raising on an unavailable registry.
Returns the keys for the given process pid.
Look up a process by key.
Returns the registry module (Registry or Horde.Registry).
Returns the registry name atom (Sagents.Registry).
Select processes matching a match specification.
Returns a :via tuple for registering or looking up a process by key.
The name of the process whose death empties this node's registry.
Functions
@spec available?() :: boolean()
Whether this node's registry can currently answer lookups.
false while the node is still starting Sagents.Supervisor, and after that
supervisor has shut down while the BEAM drains during a rolling deploy.
Checks the ETS table each backend actually reads, rather than the registry process name, because the table is what the read touches and what its owner takes with it when it dies.
Examples
if Sagents.ProcessRegistry.available?() do
# this node can host and route agent sessions
end
Returns the child spec for the configured registry backend.
Used in Sagents.Application supervision tree.
Returns the count of all registered entries.
Raises Sagents.RegistryUnavailableError when the registry cannot answer, so
a draining node never reports a truthful-looking 0.
@spec ensure_available!(atom() | nil) :: :ok
Raise Sagents.RegistryUnavailableError unless the registry can answer.
Used by the functions here whose return shape cannot carry the condition.
operation names the caller and appears in the message.
Look up a single process by key, without raising on an unavailable registry.
This is the form to use on request paths. The three outcomes are kept distinct on purpose:
{:ok, pid}- registered and alive{:error, :not_registered}- the registry answered, nothing is registered{:error, :registry_unavailable}- the registry could not answer at all
The second and third must stay distinct in whatever the caller does next. "Nothing is registered" means start one; "cannot answer" means this node cannot know, so it must not guess, because guessing produces a duplicate agent for a conversation that already has one on another node.
Examples
case Sagents.ProcessRegistry.fetch({:agent_server, "agent-123"}) do
{:ok, pid} -> GenServer.call(pid, :get_status)
{:error, :not_registered} -> {:error, :agent_not_running}
{:error, :registry_unavailable} = error -> error
end
Returns the keys for the given process pid.
Raises Sagents.RegistryUnavailableError when the registry cannot answer, so
a draining node never reports an empty list as though it were a real result.
Examples
Sagents.ProcessRegistry.keys(pid)
# => [{:agent_supervisor, "agent-123"}]
Look up a process by key.
Returns [{pid, value}] if found, [] otherwise.
Raises Sagents.RegistryUnavailableError when the registry cannot answer,
because [] would be indistinguishable from "not registered". Prefer
fetch/1 on request paths.
Examples
[{pid, _}] = Sagents.ProcessRegistry.lookup({:agent_server, "agent-123"})
Returns the registry module (Registry or Horde.Registry).
Returns the registry name atom (Sagents.Registry).
Select processes matching a match specification.
The match spec format is the same as Registry.select/2.
Raises Sagents.RegistryUnavailableError when the registry cannot answer, so
a draining node never reports an empty list as though it were a real result.
Examples
Sagents.ProcessRegistry.select([
{{{:agent_server, :"$1"}, :_, :_}, [], [:"$1"]}
])
Returns a :via tuple for registering or looking up a process by key.
Examples
Sagents.ProcessRegistry.via_tuple({:agent_server, "agent-123"})
# => {:via, Registry, {Sagents.Registry, {:agent_server, "agent-123"}}}
# or
# => {:via, Horde.Registry, {Sagents.Registry, {:agent_server, "agent-123"}}}
@spec watched_name() :: atom()
The name of the process whose death empties this node's registry.
This is not always registry_name/0, and the difference is what
Sagents.RegistryWatcher has to monitor.
Under :horde the two are the same: Horde.RegistryImpl is registered as
Sagents.Registry and owns its own ETS tables.
Under :local they differ. Registry.start_link/1 registers a
Registry.Supervisor under the given name, but the tables that hold
registrations belong to its Registry.Partition child. That partition can be
restarted with empty tables while the supervisor keeps running and keeps its
registered name, so watching the name would miss the failure entirely.
Watching the partition catches both: it also dies whenever its supervisor
does.
The single-partition name is safe because child_spec/1 starts the registry
without a :partitions option. Changing that would need this to return every
partition.