The leader-only fenced rebalance loop (design §5.3, §6.1–6.4;
implementation.md §2). One instance of this process exists per namespace at
a time — on whichever node holds leadership — started and stopped by
Fief.Planner.Elector on :elected/:deposed. Every write is
cas_assign/cas_settle carrying the planner's term, so a zombie planner
(deposed but not yet notified) is fenced by the StateStore's term ratchet;
duplicate planners can thrash a plan but never corrupt state.
State is derived, never handed over
The planner keeps no plan state worth surviving it: every pass re-reads
membership, live leases, and the table from the Authority and recomputes.
A successor leader resumes mid-rebalance by construction — open transfers
are prev_owner rows, pacing is the "planner SELECT" (open-row count), and
the settle trigger re-arrives via the settle relay's retry (Elector).
Liveness source
Serving membership = status :active and a live lease per
Fief.StateStore.read_leases/1 — the arbiter's own judgement of expiry,
the only thing reassignment may be gated on (design §6.4). Presence is
hint-grade: a {:fief_presence, ...} message only runs a pass sooner.
Transfer rules (design §6.3, specs/FiefTransfer.tla)
- Graceful move (
MoveGraceful): onecas_assign, opening a transfer. At mostmax_concurrent_transferstransfers open at once, counted from the table (openprev_ownerrows), so pacing survives leader failover. - Never chain (single-hop rule,
FiefTransfer_chained): a vnode with an open transfer whose donor and recipient are healthy is never touched, whatever the desired distribution says. (Fief.Authority.Localadditionally keeps the original donor if a chainedcas_assignever slipped through — M1's backstop — but the planner is the enforcement.) - Settle (
Settle): only on the donor's{:fief_settle_report, session, node}, verified against the current row before the CAS (⊨ rule: settle is donor-reported). - Recipient death mid-transfer (
ReassignToDonor): reassign back to the donor, never a third node — as the CAS pair assign + settle (the assign yields the{donor, donor}self-row, and no donor report exists for a self-session, so the planner settles it immediately; agents treat the settled row ashandoff_in(nil)— the taint path). - Donor death mid-transfer (
AbortSession): settle without a report — design §6.3: "the planner aborts the session (prev_owner → NULL) and unmigrated keys recover via escheat". This is the one settle not gated on a donor report, sanctioned by the spec as a distinct action. - Dead owner, no live donor (
ReassignDead): assign anywhere healthy with no donor — expressed as the CAS pair assign + settle (the StateStore records the old owner asprevon any reassignment; the immediate settle clears it, so nothing ever pulls from a dead hop — the single-hop rule). - Drain for leave (design §6.2):
:leavingmembers get zero desired vnodes; their vnodes move away as ordinary graceful transfers, andFief.Node's leave gate holds the node in:leavinguntil it is neither owner norprev_owneranywhere.
Arbiter loss
{:error, :unreachable} from any read or CAS freezes the pass: the planner
stops writing (it could not CAS anyway) and retries at the next cadence
tick — the on_arbiter_loss: :freeze posture, and the only planner-side
meaning of that option (:serve_last_epoch is node-side: nodes already
serve their last-observed epoch until they self-fence).
Stale writes
{:error, :stale} aborts the pass and schedules an immediate (0 ms seam
timer) re-read-and-recompute — never a blind retry. Three consecutive stale
passes mean the term itself is fenced (a newer leader ratcheted past us):
the planner emits the :planner_stalled checkpoint and falls back to the
cadence tick, waiting for the Elector's :deposed.
Registered under Fief.Seam.planner_name/1 (minted in this — sim
bound — process), the cross-node address settle reports are relayed to.
Follows THE process discipline (sim bind, :handled checkpoints, no raw
timers, stale-timer catch-all).
Summary
Functions
Returns a specification to start this module under a supervisor.
Compute one pass's CAS actions from an authoritative snapshot. Pure and deterministic given the same inputs — the property leader failover leans on.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec plan(map(), [{term(), atom()}], [term()], pos_integer(), pos_integer()) :: [ {:assign, non_neg_integer(), term()} | {:settle, non_neg_integer()} ]
Compute one pass's CAS actions from an authoritative snapshot. Pure and deterministic given the same inputs — the property leader failover leans on.
Strategy (capacity-capped keep-first greedy, documented as the as-built
decision): serving members (status :active, lease live) are sorted; each
gets a capacity of partitions ÷ n (+1 for the first rem members). In
vnode order, a settled vnode keeps its owner while the owner is serving and
under capacity — minimal movement — and every other vnode (unassigned, dead
owner, leaving owner, over-capacity owner) is filled onto the first serving
member with spare capacity. Recovery actions (dead recipients/donors) run
first and never consume transfer budget; graceful moves are capped by
max_transfers minus the transfers still open after recovery.
Returns [{:assign, vnode, owner} | {:settle, vnode}], execution-ordered.