portunus_registry (portunus v0.9.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 stops the local election; the child is gone cluster-wide once every node that added it calls remove/2, and a remove/2 on the owner alone moves the child to another node. sync/2 reconciles the whole set in one call: a consumer that mirrors an external source of truth should use it, because an add-only reconcile resurrects deleted children on a node that was offline at delete time.

No new replicated state: the registry holds only local election pids and a local supervisor. 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, _}, and the restarted registry is empty: the host re-adds its children, as it does after a node restart.

The registry's cleanup releases cluster-wide locks, so a child spec supervising it should give it a generous shutdown value.

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. 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.

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.

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. 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.

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}].