sumanpal97 opened a new pull request, #23168: URL: https://github.com/apache/kafka/pull/23168
On a clean shutdown the leader sends `EndQuorumEpoch` carrying a list of preferred successors ordered by log end offset, and each successor waits a backoff proportional to its position before standing for election. The first entry, being the most up to date, is expected to win. It cannot. `FollowerState.canGrantVote()` rejects a pre-vote whenever `hasFetchedFromLeader` is true, without accounting for the leader having already announced that it is resigning. With more than three voters the first successor collects only its own vote and the resigned leader's, which is not a majority, so it loses and returns to follower. The second entry then wins, because by that point the first entry has transitioned away from follower and is able to vote. The pattern repeats for larger quorums: the third entry wins with six or seven voters, the fourth with eight or nine. Every graceful controller shutdown therefore wastes an election round, which is the degraded availability described in the JIRA. This change tracks whether the leader of the current epoch has resigned, and lets a follower grant a pre-vote in that case even if it has already fetched, as long as the requester's log is up to date. Granting is no longer disruptive once the leader has stepped down, so the disruptive-candidate protection introduced by KIP-996 is not weakened for a healthy leader. Standard votes are unaffected, since the new condition sits behind the existing `isPreVote` guard. The flag lives on `FollowerState` and needs no explicit reset: `QuorumState` allocates a fresh `FollowerState` on every transition, so it is naturally scoped to one epoch and leader, exactly like the existing `hasFetchedFromLeader`. It is set in `handleEndQuorumEpochRequest` only when the replica is still a follower and the request came from its own leader. ### Testing strategy `RaftEventSimulationTest.preferredSuccessorWinsElectionAfterGracefulLeaderShutdown` is a multi-node simulation that starts a quorum, waits for a leader and for every replica to converge on the same log so the resigning leader is able to grant pre-votes, reads the ranking the leader will advertise via `LeaderState.nonLeaderVotersByDescendingFetchOffset()`, shuts the leader down gracefully, and asserts that the first entry of that advertised ranking becomes the new leader. The ranking is read from the leader rather than hardcoded, so the assertion does not depend on which node happens to be elected first. It is parameterized over four through nine voters. Without the production change it fails for all six sizes, and the winner lands at exactly the position the JIRA predicts: the second entry for four and five voters, the third for six and seven, the fourth for eight and nine. With the change all six elect the first entry, and the election completes in roughly 30 ms instead of waiting out a backoff. Nine voters is the upper bound because `strictExponentialElectionBackoffMs` right-shifts the configured maximum by the number of successors, so beyond that the base backoff falls below the simulation's polling interval and the successors no longer stand for election one at a time. The test also raises `election.backoff.max.ms` to the production default via a new mutable field on the simulation's `Cluster`; the other simulations lower it to keep their runtime down, but here the gap between successive candidates is the behavior under test. An `@ParameterizedTest` was used rather than the class's `withVotersObservers` helper because that helper randomizes both the seed and the voter count on every run, whereas this scenario needs a deterministic case per quorum size. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
