portunus_election behaviour (portunus v0.14.0)
View SourceHelps implement a leader election with application-specific semantics.
A candidate (an election participant) runs on every node. At most one candidate is elected at a time (at any committed Raft index).
When a candidate is elected, the elected/1 callback is called.
Its opposite, stepped_down/1, is invoked when the participant
loses leadership.
-callback elected(Ctx :: election_ctx()) -> {ok, State :: term()}.
-callback stepped_down(State :: term()) -> ok.Ctx is an election_ctx() map carrying name, key, token, and
args, so the elected leader can use the fencing token for operations
on external resources.
Summary
Functions
Whether this participant is the elected owner. An election blocked in a
Ra command (a quorum loss, a slow elected/1) is not the owner, so a
caller that must not block treats a timeout as false.
Stop this election's local work for a planned transfer to TargetNode and
return {ok, LockKey, Token} for the caller to commit in a batch. It runs
transfer_to/2's ready-contender pre-check first, so a not-ready target is
refused with {error, {no_contender, TargetNode}} before any work stops, and
a standby is {error, not_owner}.
Hand a prepared election its committed per-item result: ok re-contends as a
standby, {error, {no_contender, _}} restores the local work on the unchanged
token, and {error, not_owner} re-contends because the lease lapsed during
the command. A settle arriving after the election has already moved on (a
lease_lost, or the reconciliation backstop) is a no-op.
Stop several elections concurrently against one deadline, killing
stragglers. Each election's terminate runs user stepped_down code plus a
revoke that blocks up to the command timeout under no quorum, so a serial
stop holds the caller for the sum. A killed election's revoke is lost; TTL
expiry covers it.
Move several keys owned by this node to their targets in one batched command.
This is transfer_to/2's choreography run once for the whole batch instead of
per key: prepare stops each key's local work and gathers its token, a single
portunus:transfer_many/2 command commits them all, and settle re-contends or
restores each key from its per-item result.
Ask this node's election, if it is the current owner of its key, to hand
ownership to TargetNode. It pre-checks that TargetNode is a ready
contender, stops the local work, issues the token-fenced transfer, and on
success re-contends as a standby; if the target was not ready it restores the
local work and stays owner. Returns {error, not_owner} when this node is not
the owner, and {error, {no_contender, TargetNode}} when the target is not a
ready contender. {error, no_quorum} means the command timed out and its
outcome is unknown: the work stays stopped while the election settles
ownership itself (restoring it or re-contending), so the caller retries later
rather than treating it as a failed transfer. A retry made before that
settles also returns {error, not_owner}; it does not prove ownership moved.
Types
-type election_ctx() :: #{name := portunus:name(), key := portunus:lock_key(), token := portunus:token(), args := term()}.
-type election_opts() :: #{ttl_ms => pos_integer(), affinity => portunus_affinity:spec()}.
Callbacks
-callback elected(election_ctx()) -> {ok, State :: term()}.
-callback stepped_down(State :: term()) -> ok.
Functions
Whether this participant is the elected owner. An election blocked in a
Ra command (a quorum loss, a slow elected/1) is not the owner, so a
caller that must not block treats a timeout as false.
-spec prepare_transfer(pid(), node()) -> {ok, portunus:lock_key(), portunus:token()} | {error, {no_contender, node()} | not_owner | no_quorum}.
Stop this election's local work for a planned transfer to TargetNode and
return {ok, LockKey, Token} for the caller to commit in a batch. It runs
transfer_to/2's ready-contender pre-check first, so a not-ready target is
refused with {error, {no_contender, TargetNode}} before any work stops, and
a standby is {error, not_owner}.
The election then keeps its lease renewing but stays a follower until
settle_transfer/2 (or, if none arrives, a reconciliation read) resolves who
owns the key, so between prepare and settle the key runs on no node.
Hand a prepared election its committed per-item result: ok re-contends as a
standby, {error, {no_contender, _}} restores the local work on the unchanged
token, and {error, not_owner} re-contends because the lease lapsed during
the command. A settle arriving after the election has already moved on (a
lease_lost, or the reconciliation backstop) is a no-op.
-spec start_link(portunus:name(), portunus:lock_key(), module(), term()) -> {ok, pid()} | {error, term()}.
-spec start_link(portunus:name(), portunus:lock_key(), module(), term(), election_opts()) -> {ok, pid()} | {error, term()}.
-spec stop(pid()) -> ok.
-spec stop_all([pid()]) -> ok.
Stop several elections concurrently against one deadline, killing
stragglers. Each election's terminate runs user stepped_down code plus a
revoke that blocks up to the command timeout under no quorum, so a serial
stop holds the caller for the sum. A killed election's revoke is lost; TTL
expiry covers it.
-spec stop_all([pid()], pos_integer()) -> ok.
-spec transfer_many(portunus:name(), [{Key, node()}], #{Key => pid()}) -> [{Key, ok | {error, term()}}] when Key :: term().
Move several keys owned by this node to their targets in one batched command.
This is transfer_to/2's choreography run once for the whole batch instead of
per key: prepare stops each key's local work and gathers its token, a single
portunus:transfer_many/2 command commits them all, and settle re-contends or
restores each key from its per-item result.
KeyTargets names, per key, the node it should move to. LiveElections maps
each of this node's keys to its election pid, so the caller (a registry or a
service) supplies only its own election map. Ahead of the command the pairs
are deduplicated by key, a pair whose target is this node is ok without
touching its election, and a key with no live election here is
{error, not_owner}; a very large batch is chunked into bounded commands.
Returns one {Key, ok | {error, term()}} per distinct key, in input order.
-spec transfer_to(pid(), node()) -> portunus:ok_or_error({no_contender, node()} | not_owner | no_quorum).
Ask this node's election, if it is the current owner of its key, to hand
ownership to TargetNode. It pre-checks that TargetNode is a ready
contender, stops the local work, issues the token-fenced transfer, and on
success re-contends as a standby; if the target was not ready it restores the
local work and stays owner. Returns {error, not_owner} when this node is not
the owner, and {error, {no_contender, TargetNode}} when the target is not a
ready contender. {error, no_quorum} means the command timed out and its
outcome is unknown: the work stays stopped while the election settles
ownership itself (restoring it or re-contending), so the caller retries later
rather than treating it as a failed transfer. A retry made before that
settles also returns {error, not_owner}; it does not prove ownership moved.