Class Succession
The rules are pure. They read a RoleState, one instant, and a
LeaseConfig, and they perform no input and no output. A test drives a whole
failover with no broker and no sleep.
The epoch is the safety mechanism
Kafka's transaction coordinator mints the leadership epoch when a member calls
InitProducerId for the transactional.id of a role. The quorum picks
the value, the value only grows, and every broker rejects a write that carries an
older one. That fence is the whole of the safety story. The rules here decide
when a member calls InitProducerId. They never decide who is
authoritative, because the coordinator decides that. A wrong decision makes a
failover early or late, and it costs epoch churn. It never makes two members
authoritative for one role.
Rank comes from the log, not from configuration
A candidate appends a registration record to CoordinationCodec.TOPIC. The
offset of that record in the partition is the join sequence of the candidate. Log
compaction keeps the offset of every record it retains, so a reader that walks the
partition in offset order sees the registrations in registration order. A recovered
node registers again, takes a higher offset, and lands at the tail of the roster.
That is the no-failback rule, and it needs no counter and no coordinator.
The stagger is an optimisation
A challenger of rank n waits n challenge staggers past the deadline
of the lease. This saves epoch churn, and it does nothing else.
InitProducerId is atomic at the coordinator, so a simultaneous challenge by
every standby still leaves exactly one member with the newest epoch. The losers keep
an older epoch, and each one learns that it lost on its first guarded write. Set the
stagger to make that outcome rare. Do not set it to make the outcome safe, because
the outcome is already safe.
Example
RoleState state = RoleState.fromRecords(role, transport.readRoleRecords(role));
Decision decision = Succession.evaluate(state, me, clock.nowMillis(), config);
if (decision.action() == Decision.Action.CHALLENGE) {
FencingToken token = transport.acquireEpoch(role);
}
-
Method Summary
Modifier and TypeMethodDescriptionstatic Decisionevaluate(RoleState state, MemberId member, long nowMillis, LeaseConfig config) Decides what one member does about a role at one instant.
-
Method Details
-
evaluate
public static Decision evaluate(RoleState state, MemberId member, long nowMillis, LeaseConfig config) Decides what one member does about a role at one instant.The rules are:
- A member that is not in the roster gets
Decision.Action.NOT_REGISTERED. It has no rank, because rank comes from the registration record. - A member that the lease names, while that lease is live, gets
Decision.Action.HOLD. - A challenger of rank
ngetsDecision.Action.CHALLENGEfrom the deadline plusnchallenge staggers on. With no lease, the anchor is the registration instant of the member instead of a deadline, so rank 0 challenges at once and ranknchallengesnstaggers later. - Every other member gets
Decision.Action.WAIT. The instant it carries is the earliest instant at which this answer changes for an unchanged role state.
The anchor of rule 3 always comes from the role state, and never from the current instant. A member that has no lease to wait for anchors on its own registration record. An anchor of "now plus
nstaggers" would move forward on every evaluation, and a standby of rank 1 or more would then wait for ever while rank 0 is dead.The member of an expired lease keeps rank 0, so it reclaims its own role at its own deadline. See
RoleState.rankOf(MemberId).A caller evaluates again when it reads a new record, and at the latest at the instant that
Decision.waitUntilMillis()names.- Parameters:
state- the folded state of the rolemember- the member that asksnowMillis- the instant to decide at, in milliseconds since the Unix epochconfig- the lease timings of the role- Returns:
- the step the member takes now
- Throws:
NullPointerException- if the state, the member, or the config is null
- A member that is not in the roster gets
-