portunus_election behaviour (portunus v0.14.0)

View Source

Helps 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

election_ctx()

-type election_ctx() ::
          #{name := portunus:name(),
            key := portunus:lock_key(),
            token := portunus:token(),
            args := term()}.

election_opts()

-type election_opts() :: #{ttl_ms => pos_integer(), affinity => portunus_affinity:spec()}.

Callbacks

elected/1

-callback elected(election_ctx()) -> {ok, State :: term()}.

stepped_down(State)

-callback stepped_down(State :: term()) -> ok.

Functions

handle_call/3

handle_cast/2

handle_info/2

init/1

is_leader(Pid)

-spec is_leader(pid()) -> boolean().

is_leader(Pid, Timeout)

-spec is_leader(pid(), timeout()) -> boolean().

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.

prepare_transfer(Pid, TargetNode)

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

settle_transfer(Pid, Result)

-spec settle_transfer(pid(), ok | {error, term()}) -> ok.

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.

start_link(Name, Key, Mod, Args)

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

start_link(Name, Key, Mod, Args, Opts)

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

stop(Pid)

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

stop_all(Pids)

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

stop_all(Pids, TimeoutMs)

-spec stop_all([pid()], pos_integer()) -> ok.

terminate/2

transfer_many(Name, KeyTargets, LiveElections)

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

transfer_to(Pid, TargetNode)

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