Malachi.Cluster.LeaseHolder (malachi v0.8.13)

View Source

The lease client (R0-b): a GenServer that keeps trying to hold a Malachi.Cluster.Lease and runs the leader-election timer triangle (Kubernetes-style), so exactly one node runs the fenced work.

On each :retry_period_ms tick it calls the injected renew seam (acquire-or-renew):

  • a follower that acquires becomes leader and calls on_acquired with the fencing token;
  • a leader that renews stays leader (recording the local time of the successful renewal);
  • a leader told the lease is held by another drops leadership immediately (on_lost);
  • a leader that cannot reach the lease keeps trying until :renew_deadline_ms has passed since its last successful renewal, then proactively drops leadership (on_lost), the k8s OnStoppedLeading: give up before the lease could expire and be stolen, so two nodes never both believe they lead.

The triangle must satisfy lease duration > renew_deadline_ms > retry_period_ms: the holder gives up (deadline) before the lease expires (duration), leaving a safety margin for another node to take over; it retries several times (retry period) within the deadline. Elapsed time is measured on the local clock (:clock), tolerating clock skew as k8s does (assumes NTP). On normal shutdown a leader releases the lease, so the next holder can take over without waiting for expiry.

Seams (all injected, so the timing logic is testable without ra):

  • :renew - (-> {:ok, fence} | {:error, :held} | {:error, reason}) (:held means another node holds the lease; any other error means it could not be reached/renewed this tick);

  • :release - (fence -> any), best-effort release on shutdown;
  • :on_acquired - (fence -> any), leadership gained;
  • :on_lost - (-> any), leadership lost/dropped;
  • :retry_period_ms (default 2_000), :renew_deadline_ms (default 10_000);
  • :clock - (-> integer()) monotonic ms (default System.monotonic_time/1);
  • :name - optional registered name.

Summary

Functions

Returns a specification to start this module under a supervisor.

Whether this node currently holds the lease. A plain read, does not force a tick.

Starts the holder. See the module doc for required seams.

Runs one election tick synchronously; returns {role, fence} (a manual trigger, e.g. for tests).

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

leader?(server)

@spec leader?(GenServer.server()) :: boolean()

Whether this node currently holds the lease. A plain read, does not force a tick.

start_link(opts)

@spec start_link(keyword()) :: GenServer.on_start()

Starts the holder. See the module doc for required seams.

tick_now(server)

@spec tick_now(GenServer.server()) :: {:follower | :leader, non_neg_integer() | nil}

Runs one election tick synchronously; returns {role, fence} (a manual trigger, e.g. for tests).