| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
Eventually Consistent distributed process registry, process groups, lifecycle monitoring, and isolated subclusters for Elixir. No external dependencies.
def deps do
[{:group, "~> 0.2.0"}]
endStart a Group instance under your supervision tree:
children = [
{Group, name: :my_app}
]# Register the calling process under a unique key
:ok = Group.register(:my_app, "user/123", %{name: "Alice"})
# Look up by key — returns {pid, meta} or nil
{pid, %{name: "Alice"}} = Group.lookup(:my_app, "user/123")
# Unregister (also happens automatically on process death)
:ok = Group.unregister(:my_app, "user/123")# Join a group (many processes can join the same key)
:ok = Group.join(:my_app, "chat/room/42", %{role: :member})
# List all members — returns [{pid, meta}, ...]
members = Group.members(:my_app, "chat/room/42")
# Read at most one arbitrary member without materializing the full group
[member] = Group.members(:my_app, "chat/room/42", limit: 1)
# Read only members whose owning process is on this node
local_members = Group.local_members(:my_app, "chat/room/42", limit: 10)
# Leave
:ok = Group.leave(:my_app, "chat/room/42")members/2 and local_members/2 return joined processes for a key. Registered processes are not included — use lookup/2 for those. Keys ending with "/" perform a prefix query across all shards:
# All members in rooms under "chat/"
Group.members(:my_app, "chat/")Subscribe to lifecycle events matching a pattern:
# Prefix match — all keys starting with "user/"
:ok = Group.monitor(:my_app, "user/")
# Exact match
:ok = Group.monitor(:my_app, "user/123")
# Everything
:ok = Group.monitor(:my_app, :all)Events arrive as {:group, events, info} tuples in the monitoring process's mailbox:
def handle_info({:group, events, _info}, state) do
Enum.each(events, fn
%Group.Event{type: :registered, key: key, pid: pid, meta: meta} ->
# a process registered at `key`
:ok
%Group.Event{type: :unregistered, key: key, meta: meta, reason: reason} ->
# a registered process died or unregistered
:ok
%Group.Event{type: :joined, key: key, pid: pid, meta: meta} ->
# a process joined the group at `key`
:ok
%Group.Event{type: :left, key: key, pid: pid, meta: meta, reason: reason} ->
# a process left or died
:ok
end)
{:noreply, state}
endSingle operations (register, join) produce one event per tuple. Bulk operations (nodedown, process death) batch all events from that operation into one tuple.
Send a message to all members of a key:
:ok = Group.dispatch(:my_app, "chat/room/42", {:new_message, "hello"})
:ok = Group.dispatch(:my_app, "chat/room/42", {:new_message, "hello"}, cluster: "servers_123")Compared to Phoenix.PubSub, dispatch only broadcasts to nodes with at least one subscription and can also be tailored to a given cluster.
Isolate groups and registries into named subclusters. Only nodes that have called connect/2 for a cluster participate in that cluster's replication.
# Connect this node to a named cluster
:ok = Group.connect(:my_app, "game_servers_123")
# Or lease the connection while this node still has local interest in it
:ok = Group.connect(:my_app, "game_servers_123", ttl: 30_000)
# All operations accept a :cluster option
:ok = Group.join(:my_app, "room/1", %{}, cluster: "game_servers_123")
members = Group.members(:my_app, "room/1", cluster: "game_servers_123")
:ok = Group.monitor(:my_app, :all, cluster: "game_servers_123")TTL leases are local policy only:
# All Group peers (nodes that completed peer discovery), excluding self
Group.nodes(:my_app)
# All nodes in a named cluster
Group.nodes(:my_app, "game_servers_123")Toggle verbose logging at runtime without restart:
Group.log_level(:my_app, :verbose) # turn on verbose
Group.log_level(:my_app, :info) # back to normal
Group.log_level(:my_app, false) # silence routine info/verbose logsGroup.log_level/2 updates :persistent_term, so it should be used as an occasional admin control, not from a hot path.
Events are delivered as {:group, events, %{name: name}} tuples containing %Group.Event{} structs:
%Group.Event{
type: :registered | :unregistered | :joined | :left,
supervisor: :my_app,
cluster: nil | "cluster_name",
key: "user/123",
pid: #PID<0.150.0>,
meta: %{},
previous_meta: nil | %{}, # old meta on re-register/re-join
reason: nil | term() # exit reason on unregistered/left
}| Event | Trigger |
|---|---|
| :registered | register/4 — new or re-register (updates meta) |
| :unregistered | Process died or unregister/3 called |
| :joined | join/4 — new or re-join (updates meta) |
| :left | Process died or leave/3 called |
Re-registering or re-joining an existing key updates the metadata in place and delivers an event with previous_meta set to the old value.
All operations are eventually consistent:
{Group,
name: :my_app,
shards: 8, # number of write shards (default)
log: :info, # :info | :verbose | false
resolve_registry_conflict: {MyResolver, :resolve, []}, # partition conflict resolver
extract_meta: {MyApp, :extract_meta, []}, # transform read/event metadata
replicated_pg_receiver_buffer_size: 64,
replicated_pg_receiver_flush_interval: 5,
replicated_registry_receiver_buffer_size: 64,
replicated_registry_receiver_flush_interval: 5,
replicated_sender_buffer_size: 64,
replicated_sender_flush_interval: 5,
busy_dist_retry_attempts: 300,
busy_dist_retry_interval: 1_000,
replicated_pg_receiver_local_request_quota: 8
}Group.Supervisor (:"my_app_group_sup") ├── Group.Replica.Data — owns ETS tables and serializes membership writes ├── Group.PeerReconnect — bounded recovery after busy distribution links ├── Group.Replica.Supervisor — supervises N shard GenServers │ ├── Group.Replica (shard 0) │ ├── Group.Replica (shard 1) │ └── ... ├── Registry — local monitor subscriptions (:"my_app_group_registry") └── Group.ClusterLease — local named-cluster TTL sweeper
Keys are routed to shards via :erlang.phash2({cluster, key}, num_shards). Including the cluster in the hash avoids false contention between the default cluster and named clusters.
Reads (lookup, members) go directly to ETS — no GenServer hop. This is the hot path and runs at millions of ops/sec.
Writes (register, join, etc.) go through the shard's GenServer, which updates ETS and broadcasts replication messages. Multiple shards reduce write contention for unrelated keys.
Each shard owns 4 ETS tables:
| Table | Type | Key | Purpose |
|---|---|---|---|
| reg_by_key | :set | {cluster, key} | Registry lookup — O(1) |
| reg_by_pid | :ordered_set | {pid, cluster, key} | Reverse index for death cleanup |
| pg_by_key | :ordered_set | {cluster, key, pid} | Group membership lookup |
| pg_by_pid | :ordered_set | {pid, cluster, key} | Reverse index for death cleanup |
Plus 3 shared tables:
cluster_nodes / node_clusters remain the authoritative cluster-membership tables. cluster_leases is only local lease metadata used by the sweeper.
Group.Replica.Data owns all tables and is supervised with rest_for_one so tables survive shard crashes.
When Group starts (or a new Erlang node connects), shards exchange peer_connect / peer_connect_ack messages with their counterparts on other nodes. This handshake:
This is how a new node catches up to the existing cluster state.
After the initial sync, steady-state changes propagate through separate sender and receiver batching lanes:
The sender flush timer is mainly a fallback for idle periods. Outbound buffers also flush immediately when they hit the configured size, when a new enqueue finds the buffer already past its flush interval, and before control or routing work such as cluster connect/disconnect or peer-protocol handling.
Named-cluster TTLs are a local way to reduce replication fanout to nodes that no longer care about a cluster.
Shards monitor all registered/joined processes. On DOWN, the shard:
On nodedown, each shard purges all entries owned by the disconnected node from its ETS tables and fires events for each removed entry.
mix testSee test/README.md for details on the distributed test infrastructure.
cd priv/bench
# Local (single-node)
./run_local.sh
# Distributed (3 separate BEAM VMs)
./run_distributed.sh
./run_distributed.sh --shards 4See priv/bench/README.md for scenario descriptions.
MIT
| Back | FazBrowse Home | New Git URL |