Sagents.ProcessRegistry (Sagents v0.12.0)

Copy Markdown

Abstraction over process registry implementations.

Supports two backends:

  • :local — Elixir's built-in Registry (single-node, zero extra deps)
  • :hordeHorde.Registry (distributed, requires the :horde dependency)

Configuration

# config/config.exs (or runtime.exs)

# Single-node (default — no config needed)
config :sagents, :distribution, :local

# Distributed cluster
config :sagents, :distribution, :horde

When :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:

: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.

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

available?()

@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

child_spec(opts)

Returns the child spec for the configured registry backend.

Used in Sagents.Application supervision tree.

count()

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.

ensure_available!(operation \\ nil)

@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.

fetch(key)

@spec fetch(term()) ::
  {:ok, pid()} | {:error, :not_registered | :registry_unavailable}

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

keys(pid)

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"}]

lookup(key)

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"})

registry_module()

Returns the registry module (Registry or Horde.Registry).

registry_name()

Returns the registry name atom (Sagents.Registry).

select(match_spec)

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"]}
])

via_tuple(key)

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"}}}

watched_name()

@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.