Sagents.Horde.MembershipManager (Sagents v0.17.1)

Copy Markdown

Keeps Horde cluster membership scoped to the nodes that actually run Sagents.Supervisor, and keeps it current as nodes come and go.

This is the membership mechanism behind:

config :sagents, :distribution, :horde
config :sagents, :horde, members: :participation

Why this exists

Horde's built-in members: :auto derives membership from Node.list([:visible, :this]) — every connected BEAM node, regardless of whether it runs Horde. In a cluster that meshes multiple service roles into one Erlang cluster, that pulls unrelated nodes into the Sagents Horde cluster: it bloats the DeltaCrdt sync fan-out (a cause of registration timeouts) and, for membership added but never reporting :alive, leaves dead entries that are never pruned.

Membership here is instead derived from participation: every node that starts Sagents.Supervisor joins an OTP :pg group, and this process adds each node to Horde's members as it joins that group and removes it once it leaves. Because a node runs Sagents.Supervisor only where the host application chose to (e.g. gated to a :web role), "nodes running Sagents" is "agent-hosting nodes" — no node-name predicate required. :pg removes a node's entry automatically on :nodedown, so dead nodes are pruned without any extra wiring.

What it manages

On startup and on every :pg join/leave it updates the members of all three Sagents Horde instances through Horde.Cluster.set_members/2 so they stay consistent:

Every update starts from what the instance holds at that moment and only adds the nodes a :pg view shows or removes the nodes :pg reported as gone. A view is never handed to Horde as the whole member set. :pg discovers the other nodes' scopes asynchronously, so for a moment after this process joins, the group it reads is a partial view of the cluster, while Horde, which also learns members through CRDT replication from its peers, may already hold the rest. Horde treats a member missing from set_members/2 as removed and drops that member's registrations cluster-wide, so applying a partial view would unregister every agent on the nodes it has not seen yet.

It is started automatically by Sagents.Supervisor (together with its :pg scope) when members: :participation is configured; you do not start it yourself.

Partitioning

When config :sagents, :horde, partition: <value> is set, the :pg group is keyed by that partition ({:sagents_members, value}). A node only joins and monitors its own partition's group, so membership is isolated per partition — e.g. nodes in one Fly.io region never become members of another region's Horde cluster, even when all nodes share one connected BEAM cluster.

Summary

Functions

Returns a specification to start this module under a supervisor.

The :pg group joined by each participating node.

The :pg group for a given partition (nil => unpartitioned base group).

The Horde clusters this manager keeps membership-consistent.

Child spec for the :pg scope this manager relies on.

The :pg scope used for participation tracking.

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

group()

The :pg group joined by each participating node.

Keyed by the configured partition (Sagents.Horde.ClusterConfig.partition/0) so nodes only cluster with same-partition peers. Unpartitioned membership uses the base group atom.

group_for(partition)

The :pg group for a given partition (nil => unpartitioned base group).

hordes()

The Horde clusters this manager keeps membership-consistent.

pg_scope_spec()

Child spec for the :pg scope this manager relies on.

Sagents.Supervisor starts this before the manager so the scope is available when the manager joins and monitors it.

scope()

The :pg scope used for participation tracking.

start_link(opts \\ [])