portunus_registry (portunus v0.14.0)

View Source

A dynamic cluster-wide supervisor. "Dynamic" isn't an Erlang/OTP supervisor type here: it means children are added and removed at runtime with add/3 and remove/2, unlike portunus_supervisor, whose children are the fixed set returned once by init/1.

add/3 registers a child spec (a supervisor:child_spec(), or one carrying an extended {permanent, Delay} restart option which portunus_delayed_restart rewrites) under a key; portunus runs one election per key, and the elected owner starts that child under a local Erlang/OTP supervisor.

remove/2 is the counterpart of add/3. A child is permanently removed from the cluster once every node applies the change.

remove/2 on the owner alone moves the child to another node.

Restart is local and per-child, with one exception: a child that crash-loops past the local supervisor's intensity takes the registry down with {local_sup_down, _}. The restarted registry is initially empty: the host re-adds its children, as it does after a node restart.

Summary

Functions

Register ChildSpec under its child id. Only one instance of it can run in the cluster at any given time.

Register ChildSpec under Key. Only one instance of it can run in the cluster at any given time. Validated at registration: an invalid spec, a re-add with a different spec, or a child id already used under another key is an error, not a later elect-and-fail loop. Re-adding the identical spec is idempotent.

The keys this node is contending for.

Keys for which this node is currently the elected owner.

Stop and forget the child keyed by Key on this node. The key is only gone cluster-wide once every node that added it calls remove/2; removing it on the owner alone moves the child to another contender.

Like start_link/2, but registers the registry process under ServerName (e.g. {local, shovel_registry}) so add/3, remove/2 and the rest can address it by that name instead of a pid.

Make this node's registrations equal ChildSpecs, keyed by child id: specs whose id is missing are added, registrations whose id is absent from the set are removed (with remove/2's semantics: this node stops contending, and ownership of a currently-owned key moves), and a changed spec is applied as remove then add. An unchanged spec is untouched, so a second sync/2 with the same set is a no-op and causes no ownership churn. "Unchanged" compares identity keys when both sides have one (see add/3), so specs with non-deterministic arguments avoid some churn. Duplicate ids and invalid specs are refused before any change is applied.

If this node currently owns Key, hand ownership to TargetNode; if not, return {error, not_owner}. Only the owner can transfer. If TargetNode is not a ready contender the owner keeps running and the reply is {error, {no_contender, TargetNode}}, never a handoff to some other node.

Move several owned keys to their targets in one batched command: the batch equivalent of transfer/3.

Validate a child spec with the same check add/3 and sync/2 apply, returning the child id of a valid spec.

The children this node currently runs (it was elected for), in supervisor:which_children/1 shape [{Id, Child, Type, Modules}].

Types

add_error()

-type add_error() ::
          {invalid_child_spec, term()} | {already_added, term()} | {duplicate_child_id, term()}.

registry_opts()

-type registry_opts() ::
          #{ttl_ms => pos_integer(),
            sup_flags => supervisor:sup_flags(),
            affinity => portunus_affinity:spec(),
            group => term()}.

server()

-type server() :: gen_server:server_ref().

Functions

add(Server, ChildSpec)

-spec add(server(), portunus_delayed_restart:child_spec_in()) -> ok | {error, add_error()}.

Register ChildSpec under its child id. Only one instance of it can run in the cluster at any given time.

add(Server, Key, ChildSpec)

-spec add(server(), term(), portunus_delayed_restart:child_spec_in()) -> ok | {error, add_error()}.

Register ChildSpec under Key. Only one instance of it can run in the cluster at any given time. Validated at registration: an invalid spec, a re-add with a different spec, or a child id already used under another key is an error, not a later elect-and-fail loop. Re-adding the identical spec is idempotent.

A spec whose args differ on every computation (encrypted credentials are the common case) can carry an identity key: a term meaning "any two specs with this value start the same child". When both sides carry one, idempotence compares identities instead of specs. The key never reaches a supervisor.

handle_call/3

handle_cast(Msg, State)

handle_info/2

init/1

keys(Server)

-spec keys(server()) -> [term()].

The keys this node is contending for.

owned_keys(Server)

-spec owned_keys(server()) -> [term()].

Keys for which this node is currently the elected owner.

remove(Server, Key)

-spec remove(server(), term()) -> ok.

Stop and forget the child keyed by Key on this node. The key is only gone cluster-wide once every node that added it calls remove/2; removing it on the owner alone moves the child to another contender.

start_link(Name, Opts)

-spec start_link(portunus:name(), registry_opts()) -> {ok, pid()} | {error, term()}.

start_link(ServerName, Name, Opts)

-spec start_link(gen_server:server_name(), portunus:name(), registry_opts()) ->
                    {ok, pid()} | {error, term()}.

Like start_link/2, but registers the registry process under ServerName (e.g. {local, shovel_registry}) so add/3, remove/2 and the rest can address it by that name instead of a pid.

stop(Server)

-spec stop(server()) -> ok.

sync(Server, ChildSpecs)

-spec sync(server(), [portunus_delayed_restart:child_spec_in()]) -> ok | {error, add_error()}.

Make this node's registrations equal ChildSpecs, keyed by child id: specs whose id is missing are added, registrations whose id is absent from the set are removed (with remove/2's semantics: this node stops contending, and ownership of a currently-owned key moves), and a changed spec is applied as remove then add. An unchanged spec is untouched, so a second sync/2 with the same set is a no-op and causes no ownership churn. "Unchanged" compares identity keys when both sides have one (see add/3), so specs with non-deterministic arguments avoid some churn. Duplicate ids and invalid specs are refused before any change is applied.

Built for reconciling children from an external source of truth: an add-only reconcile resurrects deleted children on a node that was offline at delete time. sync/2 cannot fix a stale input; a node syncing from an outdated view of the source still registers what that view says.

terminate/2

transfer(Server, Key, TargetNode)

-spec transfer(server(), term(), node()) ->
                  portunus:ok_or_error({no_contender, node()} | not_owner | no_quorum).

If this node currently owns Key, hand ownership to TargetNode; if not, return {error, not_owner}. Only the owner can transfer. If TargetNode is not a ready contender the owner keeps running and the reply is {error, {no_contender, TargetNode}}, never a handoff to some other node.

transfer_many(Server, KeyTargets)

-spec transfer_many(server(), [{term(), node()}]) -> [{term(), portunus:ok_or_error(term())}].

Move several owned keys to their targets in one batched command: the batch equivalent of transfer/3.

Each pair names a key and the node it should move to.

Keys on the list that are not owned by the caller are ignored (not moved).

Returns one {Key, ok | {error, term()}} per distinct key.

validate_spec(ChildSpec)

-spec validate_spec(term()) -> {ok, ChildId :: term()} | {error, {invalid_child_spec, term()}}.

Validate a child spec with the same check add/3 and sync/2 apply, returning the child id of a valid spec.

sync/2 refuses the entire set when one spec is invalid or two specs share a child id, so a caller that wants to sync the valid specs and report the rest validates each spec first with this function, using the returned id to drop duplicates.

Any term is accepted. Anything but a valid portunus_delayed_restart:child_spec_in/0 is an invalid_child_spec error.

which_children(Server)

-spec which_children(server()) ->
                        [{term(),
                          pid() | restarting | undefined,
                          worker | supervisor,
                          [module()] | dynamic}].

The children this node currently runs (it was elected for), in supervisor:which_children/1 shape [{Id, Child, Type, Modules}].