Current section

Files

Jump to
snakepit lib snakepit pool registry.ex
Raw

lib/snakepit/pool/registry.ex

defmodule Snakepit.Pool.Registry do
@moduledoc """
Registry for pool worker processes.
This is a thin wrapper around Elixir's Registry that provides:
- Consistent naming for worker processes
- Easy migration path to distributed registry (Horde)
- Helper functions for worker lookup
## Canonical Metadata
All workers store a metadata map containing the following canonical keys:
* `:worker_module` – module that owns the worker implementation (usually `Snakepit.GRPCWorker`)
* `:pool_name` – atom name of the logical pool (e.g. `:default`)
* `:pool_identifier` – optional human-friendly identifier used in docs/metrics
* `:adapter_module` – adapter used to launch the Python worker
Higher-level helpers (pool, diagnostics, worker profiles) should prefer
`Snakepit.Pool.Registry.fetch_worker/1` so these keys stay authoritative.
"""
alias Snakepit.Logger, as: SLog
@registry_name __MODULE__
@metadata_keys [:worker_module, :pool_name, :pool_identifier, :adapter_module]
@log_category :pool
@doc """
Returns the child spec for the registry.
"""
def child_spec(_opts) do
Registry.child_spec(
keys: :unique,
name: @registry_name
)
end
@doc """
Returns a via tuple for registering/looking up a worker process.
## Examples
iex> Snakepit.Pool.Registry.via_tuple("worker_123")
{:via, Registry, {Snakepit.Pool.Registry, "worker_123"}}
"""
def via_tuple(worker_id) when is_binary(worker_id) do
{:via, Registry, {@registry_name, worker_id}}
end
@doc """
Lists all registered worker IDs.
"""
def list_workers do
Registry.select(@registry_name, [{{:"$1", :_, :_}, [], [:"$1"]}])
end
@doc """
Checks if a worker is registered.
"""
def worker_exists?(worker_id) do
match?({:ok, _pid, _meta}, fetch_worker(worker_id))
end
@doc """
Gets the PID for a worker ID.
"""
def get_worker_pid(worker_id) do
with {:ok, pid, _metadata} <- fetch_worker(worker_id) do
{:ok, pid}
end
end
@doc """
Counts the number of registered workers.
"""
def worker_count do
Registry.count(@registry_name)
end
@doc """
Returns the list of canonical metadata keys maintained for each worker.
"""
def metadata_keys, do: @metadata_keys
@doc """
Register a worker with metadata for O(1) reverse lookups.
This is only used for manual registration - workers started with via_tuple are already registered.
"""
def register_worker(_worker_id, _pid) do
# Workers started with via_tuple are already registered automatically
# This is a no-op for compatibility
:ok
end
@doc """
Adds or updates metadata for a registered worker.
Accepts maps to keep metadata consistent across callers. When `Registry`
has `nil` metadata (the default when using `:via` tuples), this function
replaces it with the provided map. Future updates merge with the existing map.
Returns `:ok` on success or `{:error, :not_registered}` if the worker has
not been registered yet (best-effort semantics).
"""
def put_metadata(worker_id, metadata) when is_binary(worker_id) and is_map(metadata) do
sanitized = normalize_metadata(metadata)
try do
case Registry.update_value(@registry_name, worker_id, fn
current when is_map(current) -> Map.merge(current, sanitized)
_ -> sanitized
end) do
{_, _} ->
:ok
:error ->
SLog.debug(
@log_category,
"Pool.Registry.put_metadata/2 attempted to update #{inspect(worker_id)} before registration"
)
{:error, :not_registered}
end
rescue
ArgumentError ->
SLog.debug(
@log_category,
"Pool.Registry.put_metadata/2 attempted to update #{inspect(worker_id)} before registration"
)
{:error, :not_registered}
end
end
def put_metadata(_worker_id, _metadata), do: :ok
@doc """
Returns `{pid, metadata}` for a registered worker.
"""
def fetch_worker(worker_id) when is_binary(worker_id) do
case Registry.lookup(@registry_name, worker_id) do
[{pid, metadata}] ->
{:ok, pid, normalize_metadata(metadata)}
[] ->
{:error, :not_found}
end
end
def fetch_worker(_worker_id), do: {:error, :invalid_worker_id}
@doc """
Returns only the metadata for a worker.
"""
def get_worker_metadata(worker_id) do
with {:ok, _pid, metadata} <- fetch_worker(worker_id) do
{:ok, metadata}
end
end
@doc """
Get worker_id from PID for O(1) lookups in :DOWN messages.
"""
def get_worker_id_by_pid(pid) do
# Use Registry's keys/2 function for O(1) reverse lookup
case Registry.keys(@registry_name, pid) do
[worker_id] -> {:ok, worker_id}
[] -> {:error, :not_found}
end
end
defp normalize_metadata(metadata) when is_map(metadata), do: metadata
defp normalize_metadata(_metadata), do: %{}
end