DGen.Registry (DGen v0.4.0)

Copy Markdown View Source

OTP-compatible process registry backed by the configured storage backend.

Processes started with a {:via, DGen.Registry, {name, logical_name}} tuple can be addressed by name across an Erlang/Elixir cluster.

Starting a registry

{:ok, _sup} = DGen.Registry.start_link(:my_registry, tenant)

start_link/2,3 returns the supervisor's pid — hold onto it (e.g. to Supervisor.stop/2 it later) rather than looking it up by name afterward; see "Process identity" below.

Via-tuple usage

via = {:via, DGen.Registry, {:my_registry, :user_service}}
GenServer.start_link(MyServer, [], name: via)
GenServer.call(via, :ping)

Process identity

Starting a registry creates zero atoms. name is supplied by the caller and is application-controlled, not generated by this module. The supervisor itself is not registered under any name, and the elector has no registered name either — only the member is, under name directly, with no further atom derived from it. This matters for applications that start many independent registries dynamically (e.g. one per tenant): atoms are never garbage-collected, so this module never derives additional atoms per registry beyond the one the caller already chose to create.

Consistency model

register_name/2, unregister_name/1, and whereis_name_consistent/1 route through the elected leader. whereis_name/1 (used by the OTP via-tuple machinery) is served lock-free from the calling process — an ETS read against the local member's table, no member round-trip, no network hop. There is a short replication window after registration where a follower node's snapshot read may still return nil.

Metadata and queries

A registration can carry metadata for its lifetime: register_name/3 / set_metadata/2 attach an index map (queryable) and an opaque data payload; get_metadata/1 / get_metadata_consistent/1 read it back (lock-free / leader-routed, mirroring whereis_name/1 / whereis_name_consistent/1). query/2 / query_consistent/2 find every registration whose index satisfies a conjunction of exact equalities.

Summary

Functions

Blocks until ready/1 holds, or 5s elapses. Returns :ok or {:error, :timeout}.

Blocks until the registry on this node is ready to serve registrations, or timeout ms elapses. Returns :ok or {:error, :timeout}. Use after starting or joining a cluster to wait out leader election and the join handoff.

Removes all of the registry's durable backend state (membership, leader/version fence, and subscriptions). Returns :ok.

Returns the current elector pid for registry_name, or nil (:undefined) if the member (and so the elector) cannot currently be found.

Returns the current leader epoch. Increments each time a new leader is elected.

Returns the current leader member id, or :undefined if none is elected.

Returns the list of all current member ids [{node, member_atom}] in the registry.

Snapshot read of a registration's metadata — served lock-free from the calling process, mirroring whereis_name/1.

Consistent metadata read routed through the leader (authoritative, never stale).

Returns the registered name of the member process for the given registry name.

Returns the ETS table name holding the registry's local name => pid replica.

Finds every registration in registry_name whose indexed metadata satisfies all of constraints (a conjunction of exact equalities — AND-equal).

Like query/2, but routed through the leader for an authoritative (never-stale) result.

Whether the registry on this node is ready to serve registrations right now (a leader is known and this member has synced). Point-in-time; see await_ready/2.

Registers pid under {registry_name, logical_name}.

Registers pid under {registry_name, logical_name} with metadata, in one fenced step.

Sends msg to the process registered as name, returning the pid.

Replaces the metadata of the existing registration {registry_name, logical_name}.

Starts the registry name with default options.

Starts the registry name with per-registry options opts.

Creates (or updates) a durable presence subscription sub_id: keeps the processes matching notify up to date with the set of processes matching watch.

Returns the registry's durable presence subscriptions as %{sub_id => {watch, notify}} (shared cluster-wide and durable).

Removes the registration for {registry_name, logical_name}. Returns :ok.

Removes the durable presence subscription sub_id (from subscribe/4). Returns :ok (idempotent; applied asynchronously).

Removes all durable presence subscriptions from the registry. Returns :ok (applied asynchronously).

Snapshot read — served lock-free from the calling process (an ETS lookup against the local member's table, no member round-trip).

Consistent read routed through the leader.

Functions

await_ready(registry_name)

Blocks until ready/1 holds, or 5s elapses. Returns :ok or {:error, :timeout}.

await_ready(registry_name, timeout)

Blocks until the registry on this node is ready to serve registrations, or timeout ms elapses. Returns :ok or {:error, :timeout}. Use after starting or joining a cluster to wait out leader election and the join handoff.

delete(registry_name, tenant)

Removes all of the registry's durable backend state (membership, leader/version fence, and subscriptions). Returns :ok.

Stop the registry supervisor first, then call this with the tenant you started it with — a running elector would re-create the keys from its in-memory state. Clears only this registry's keys, not any other registry sharing the tenant.

elector_pid(registry_name)

Returns the current elector pid for registry_name, or nil (:undefined) if the member (and so the elector) cannot currently be found.

The elector has no registered name; it is discovered via the shared supervisor (all-local, no network hop). Exported for introspection — used internally by get_leader/1, get_epoch/1, and get_members/1.

get_epoch(registry_name)

Returns the current leader epoch. Increments each time a new leader is elected.

get_leader(registry_name)

Returns the current leader member id, or :undefined if none is elected.

get_members(registry_name)

Returns the list of all current member ids [{node, member_atom}] in the registry.

get_metadata(name)

Snapshot read of a registration's metadata — served lock-free from the calling process, mirroring whereis_name/1.

Returns {:ok, %{pid: pid, index: map, data: term}} for a registered name, or nil (:undefined) if not registered.

get_metadata_consistent(name)

Consistent metadata read routed through the leader (authoritative, never stale).

Returns the same shape as get_metadata/1.

member_name(registry_name)

Returns the registered name of the member process for the given registry name.

Identity today (member_name(name) == name) — the member is registered under name directly.

names_table(registry_name)

Returns the ETS table name holding the registry's local name => pid replica.

Identity today (names_table(name) == name). Exported for introspection; ordinary callers should use whereis_name/1 / get_metadata/1 rather than reading the table directly.

query(registry_name, constraints)

Finds every registration in registry_name whose indexed metadata satisfies all of constraints (a conjunction of exact equalities — AND-equal).

Snapshot read, local to the calling member; batch-consistent (never observes a half-applied group commit). Returns a list of %{name:, pid:, index:, data:} matches, or {:error, :empty_query} if constraints is empty.

query_consistent(registry_name, constraints)

Like query/2, but routed through the leader for an authoritative (never-stale) result.

ready(registry_name)

Whether the registry on this node is ready to serve registrations right now (a leader is known and this member has synced). Point-in-time; see await_ready/2.

register_name(name, pid)

Registers pid under {registry_name, logical_name}.

Routes through the local member, which forwards to the leader if needed. Returns :yes on success, :no if the name is already taken or no leader is currently elected.

register_name(name, pid, meta_spec)

Registers pid under {registry_name, logical_name} with metadata, in one fenced step.

meta_spec is %{index: map(), data: term()} (both optional; default %{} and nil). Like register_name/2, returns :yes on success or :no if the name is already taken or no leader is currently elected.

send(name, msg)

Sends msg to the process registered as name, returning the pid.

Raises {:badarg, {name, msg}} if the name is not registered. Called internally by OTP for {:via, …} routing.

set_metadata(name, meta_spec)

Replaces the metadata of the existing registration {registry_name, logical_name}.

meta_spec is %{index: map(), data: term()} (both optional; an omitted field becomes %{} / nil — this is a replace, not a merge). Returns :ok, {:error, :not_registered} if the name is not currently bound, or {:error, :no_leader} if no leader is elected (or leadership changed mid-flight — retry).

start_link(name, tenant)

Starts the registry name with default options.

Returns {:ok, sup_pid} — the supervisor is not registered under any name; see "Process identity" above.

start_link(name, tenant, opts)

Starts the registry name with per-registry options opts.

See docs/dgen_registry_design.md (§8 Configuration) for the available tuning knobs (register_replicas, strict_replication, etc.).

subscribe(registry_name, sub_id, watch, notify)

Creates (or updates) a durable presence subscription sub_id: keeps the processes matching notify up to date with the set of processes matching watch.

watch and notify are queries ({:all, %{attr => value}}, or a bare map read as {:all, map}). sub_id is any application term (an idempotent upsert). Whenever a committed batch changes which processes satisfy watch, every process currently matching notify receives one message {:dgen_presence, sub_id, events} where events is a non-empty list of {:joined, name, pid} / {:left, name, pid}. A process is sent an initial snapshot of the current watch set (as :joined events) whenever it enters the notify set — at subscribe time or when it registers into notify later — and deltas thereafter, so the subscription can be created before any viewer exists.

The subscription is durable — stored in the elector's backend state, so it outlives the cluster: tie its lifetime to a database entity and presence comes back intact after a scale-to-zero. Sent as a durable cast (applied asynchronously), so :ok means "accepted"; subscriptions/1 reflects it shortly after. Returns :ok, or {:error, :empty_query} if either query is empty.

subscriptions(registry_name)

Returns the registry's durable presence subscriptions as %{sub_id => {watch, notify}} (shared cluster-wide and durable).

unregister_name(name)

Removes the registration for {registry_name, logical_name}. Returns :ok.

A tracked call mirroring the register path: the local member's snapshot is updated immediately, and the removal is forwarded to the leader. :ok normally means the leader committed it; with an unreachable leader the removal is stashed and re-driven on the next leadership/rejoin event, so an explicit unregister is never silently lost (design doc, Non-goal 5).

unsubscribe(registry_name, sub_id)

Removes the durable presence subscription sub_id (from subscribe/4). Returns :ok (idempotent; applied asynchronously).

unsubscribe_all(registry_name)

Removes all durable presence subscriptions from the registry. Returns :ok (applied asynchronously).

whereis_name(name)

Snapshot read — served lock-free from the calling process (an ETS lookup against the local member's table, no member round-trip).

Never blocks on the leader or the backend. May be slightly stale on follower nodes in the brief window between a remote registration and the replication cast arriving. Returns nil (:undefined) if not found.

whereis_name_consistent(name)

Consistent read routed through the leader.

Returns the authoritative pid for {registry_name, logical_name}, or nil (:undefined) if not registered. More expensive than whereis_name/1 but never stale.