[server] Recover coordinator leader election after ZK session expiry - #4382
Kaixuan-Duan wants to merge 3 commits into
Conversation
…is garbage-collected When the ZooKeeper session of the coordinator expires, ZooKeeper deletes the ephemeral LeaderLatch node and may then garbage-collect the empty container election parent /coordinators/election. On reconnection, the Curator 5.4.0 LeaderLatch silently ignores the NONODE result of listing the election path and stays permanently idle, so a single-coordinator cluster remains without a leader until it is restarted (upstream CURATOR-724, fixed in Curator 5.8.0 which Fluss does not shade yet). Recreate the election parent as a persistent znode on RECONNECTED before the latch's own reconnection handling, so the latch can re-create its election node and the coordinator regains leadership without a restart. The recovery piggybacks on a LeaderLatch subclass since Curator notifies connection-state listeners in ConcurrentHashMap iteration order, which does not follow registration order. Add a regression test that kills the ZK session and deletes the empty election parent, verifying that leadership is regained without restart; it fails with a timeout before the fix and passes after. Co-Authored-By: Qoder <noreply@qoder.com> AI-Model: Qoder Auto AI-Contributed/Feature: 73/73 AI-Contributed/UT: 93/93
| .create() | ||
| .creatingParentsIfNeeded() | ||
| .withMode(CreateMode.PERSISTENT) | ||
| .forPath(electionPath); |
There was a problem hiding this comment.
The parent repair still leaves a race: checkExists() may find an empty CONTAINER, which ZooKeeper deletes before the existing latch calls getChildren() through line 347. In the bundled Curator implementation, that NONODE result does not trigger reset(), so election recovery can stop despite a healthy connection.
I would suggest recovering at the election lifecycle level: once the session is lost, abandon the old election participation and start a fresh LeaderLatch after connectivity returns.
The relevant scenarios are:
- SUSPENDED → RECONNECTED without LOST: retain the existing latch, because its session and candidate node may still be valid. Let the latch revalidate leadership.
- LOST: discard the old election participation, whether this coordinator was leader or standby. Its ephemeral candidate no longer represents a valid participation.
- Connectivity restored after LOST: start a fresh latch, register a candidate belonging to the current session, and determine leadership through the normal election.
- Another coordinator wins: remain standby and continue participating; reconnection does not imply leadership.
- Election or leader initialization fails: remain non-serving, with a path to another election attempt rather than becoming permanently idle.
The key invariants are:
- A lost session's election state must never authorize leadership in a later session.
- Recovery must establish a valid candidate for the current session before relying on the candidate list to determine leadership.
- Only the current election attempt may grant or revoke local leader authority.
- Election victory alone is insufficient: Fluss's epoch fencing and leader initialization must succeed before serving as leader.
- Recoverable failure must not require an unrelated future reconnect to make progress.
A fresh latch enters reset() and creates its EPHEMERAL_SEQUENTIAL candidate using creatingParentContainersIfNeeded(). The bundled Curator create path handles NONODE by recreating the parents and retrying candidate creation. It reads children after creation succeeds; while that candidate remains present, its parent cannot be collected as an empty container.
This addresses the missing-parent race through normal candidate registration, without modifying shaded Curator or repairing the parent separately.
There was a problem hiding this comment.
Addressed by restarting the election at the lifecycle level instead of repairing the parent.
- On
LOST, the participation of the old latch is discarded: itsnotLeaderhandling revokes local leadership first, andrestartElection()closes the stale latch once the connection returns. - On the first
RECONNECTEDafter aLOST, a freshLeaderLatchis started: it registers a newEPHEMERAL_SEQUENTIALcandidate for the current session viacreatingParentContainersIfNeeded()— the Curator create path recreates a missing election parent and retries, so the check-then-GC race is gone by construction, and the non-empty parent can no longer be collected. SUSPENDED → RECONNECTEDwithoutLOSTkeeps the existing latch, which revalidates leadership with the same election node.- Reconnection never implies leadership: the fresh latch competes in the normal election; if another coordinator wins, this one stays standby and keeps participating, and becoming leader still goes through epoch fencing (
fenceBecomeCoordinatorLeader) before serving. - Election restarts, initialization, and cleanup are serialized on a single callback thread, so only the current election attempt grants or revokes local leader authority.
When the ZooKeeper session of the coordinator expires, ZooKeeper deletes the ephemeral election node, so the latch's election participation becomes stale: the shaded Curator 5.4.0 LeaderLatch then fails to recover if the empty election parent is also garbage-collected, silently ignoring the NONODE result of listing the election path, and a single-coordinator cluster stays without a leader until restart. Instead of repairing the election parent, restart the election at the lifecycle level: when the session is lost, abandon the old participation and, once the connection returns, start a fresh LeaderLatch. The fresh latch registers a new election node for the current session with parents created as needed — the Curator create path recovers a missing election parent — and determines leadership through the normal election, including epoch fencing. A short suspension that keeps the session does not restart the election; the existing latch revalidates leadership on reconnection. Add a regression test that kills the ZK session and deletes the empty election parent, verifying that leadership is regained without restart.
Purpose
Linked issue: close #4362
Brief change log
Root cause: when the ZooKeeper session of the coordinator expires (e.g. after a long GC pause), ZooKeeper deletes the ephemeral LeaderLatch node, and the now-empty election parent
/coordinators/election(a container znode created by the latch) can then be garbage-collected by ZooKeeper. On reconnection, the LeaderLatch of the shaded Curator 5.4.0 lists the election path and gets a NONODE result, which is silently ignored — the latch stays permanently idle (fixed upstream by CURATOR-724 in Curator 5.8.0, which Fluss does not shade yet), so a single-coordinator cluster remains without a leader until it is restarted.Changes:
CoordinatorLeaderElectionnow uses aLeaderLatchsubclass (RecoveringLeaderLatch) that recreates the election parent onRECONNECTEDbefore the latch's own reconnection handling, so the latch re-creates its election node and the coordinator regains leadership without a restart.RECONNECTEDevent.handleStateChangeinstead of registering a separate connection-state listener, because Curator notifies connection-state listeners inConcurrentHashMapiteration order, which does not follow registration order and would not guarantee the recovery runs before the latch's own reconnection handling.fenceBecomeCoordinatorLeaderwith epoch increment.Tests
CoordinatorHighAvailabilityITCase#testRegainsLeadershipAfterSessionExpirationWithElectionParentDeleted: kills the Coordinator's ZK session and deletes the now-empty election parent (simulating ZooKeeper's container GC), then asserts that leadership is regained without a restart. The test fails with a timeout before the fix and passes after it; a control run that kills the session without deleting the parent recovers fine, which isolates the parent deletion as the cause.CoordinatorHighAvailabilityITCase7/7,CoordinatorLeaderElectionTest4/4.API and Format
Documentation