diff --git a/fluss-server/src/main/java/org/apache/fluss/server/coordinator/CoordinatorLeaderElection.java b/fluss-server/src/main/java/org/apache/fluss/server/coordinator/CoordinatorLeaderElection.java index 0ba51dafe0..336cdae4d7 100644 --- a/fluss-server/src/main/java/org/apache/fluss/server/coordinator/CoordinatorLeaderElection.java +++ b/fluss-server/src/main/java/org/apache/fluss/server/coordinator/CoordinatorLeaderElection.java @@ -22,8 +22,11 @@ import org.apache.fluss.exception.CoordinatorEpochFencedException; import org.apache.fluss.server.zk.ZooKeeperClient; import org.apache.fluss.server.zk.data.ZkData; +import org.apache.fluss.shaded.curator5.org.apache.curator.framework.CuratorFramework; import org.apache.fluss.shaded.curator5.org.apache.curator.framework.recipes.leader.LeaderLatch; import org.apache.fluss.shaded.curator5.org.apache.curator.framework.recipes.leader.LeaderLatchListener; +import org.apache.fluss.shaded.curator5.org.apache.curator.framework.state.ConnectionState; +import org.apache.fluss.shaded.curator5.org.apache.curator.framework.state.ConnectionStateListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,6 +55,11 @@ *
A lost ZooKeeper session deletes the election node of the latch, so its participation is + * stale. The election is then restarted with a fresh latch that registers a new election node for + * the next session. A connection suspension that keeps the session does not restart the election, + * because the election node of the latch is still valid. + * *
Leadership callbacks and state transitions are serialized by {@code leaderCallbackExecutor}.
* The state machine is:
*
@@ -77,7 +85,10 @@ public class CoordinatorLeaderElection implements AutoCloseable {
private static final long DEFAULT_CLOSE_TIMEOUT_MS = 10000L;
private final String serverId;
- private final LeaderLatch leaderLatch;
+ private final CuratorFramework curatorClient;
+
+ /** The latch participating in the election, replaced when the ZooKeeper session was lost. */
+ private volatile LeaderLatch leaderLatch;
// Single-threaded executor to run leader init/cleanup callbacks outside Curator's EventThread.
// Curator's LeaderLatchListener callbacks run on its internal EventThread; performing
// synchronous ZK operations there causes deadlock because ZK response dispatch also
@@ -90,8 +101,21 @@ public class CoordinatorLeaderElection implements AutoCloseable {
private final AtomicBoolean closing = new AtomicBoolean(false);
private volatile State state = State.INITIAL;
+ private volatile LeaderLatchListener latchListener;
private volatile Consumer After the first election, the server will continue to participate in future elections.
- * When re-elected as leader, the initLeaderServices callback will be invoked again.
+ * When re-elected as leader, the initLeaderServices callback will be invoked again. When the
+ * ZooKeeper session is lost, the election is restarted with a fresh latch.
*
* @param initLeaderServices the callback to initialize leader services once elected
* @param cleanupLeaderServices the callback to clean up leader services when losing leadership
@@ -129,7 +150,7 @@ public CoordinatorLeaderElection(ZooKeeperClient zkClient, String serverId) {
public void startElectLeaderAsync(
Runnable initLeaderServices, Consumer The lost session deletes the election node of the latch, so its participation is stale.
+ * Local leadership is revoked before the stale latch is abandoned, because the leadership of a
+ * lost session must never survive into the election of the next session. The fresh latch
+ * registers a new election node with parents created as needed, which also recreates an
+ * election parent that was garbage-collected while empty.
+ */
+ private void restartElection() {
+ LOG.info(
+ "Coordinator server {}: ZooKeeper session was lost, restarting leader election.",
+ serverId);
+ becomeStandby();
+ closeLatch(leaderLatch);
+ startLatch();
+ }
+
+ /** Closes a latch that is still participating in the election. */
+ private void closeLatch(LeaderLatch latch) {
+ if (latch == null || latch.getState() != LeaderLatch.State.STARTED) {
+ return;
+ }
+ try {
+ latch.close();
+ } catch (Exception e) {
+ LOG.error("Failed to close LeaderLatch for server {}.", serverId, e);
+ }
}
@Override
@@ -155,11 +229,8 @@ public void close() {
LOG.info("Closing LeaderLatch for server {}.", serverId);
if (closing.compareAndSet(false, true)) {
- try {
- leaderLatch.close();
- } catch (Exception e) {
- LOG.error("Failed to close LeaderLatch for server {}.", serverId, e);
- }
+ curatorClient.getConnectionStateListenable().removeListener(sessionLossListener);
+ closeLatch(leaderLatch);
// Events submitted after closing starts are ignored by their executor-side check.
// Since the executor is single-threaded, this task runs after all leadership work
diff --git a/fluss-server/src/test/java/org/apache/fluss/server/coordinator/CoordinatorHighAvailabilityITCase.java b/fluss-server/src/test/java/org/apache/fluss/server/coordinator/CoordinatorHighAvailabilityITCase.java
index 20f34e3ad2..09aeb66e0f 100644
--- a/fluss-server/src/test/java/org/apache/fluss/server/coordinator/CoordinatorHighAvailabilityITCase.java
+++ b/fluss-server/src/test/java/org/apache/fluss/server/coordinator/CoordinatorHighAvailabilityITCase.java
@@ -43,6 +43,7 @@
import org.apache.fluss.server.zk.ZooKeeperExtension;
import org.apache.fluss.server.zk.data.CoordinatorAddress;
import org.apache.fluss.server.zk.data.LeaderAndIsr;
+import org.apache.fluss.server.zk.data.ZkData;
import org.apache.fluss.shaded.curator5.org.apache.curator.framework.CuratorFramework;
import org.apache.fluss.shaded.zookeeper3.org.apache.zookeeper.KeeperException;
import org.apache.fluss.shaded.zookeeper3.org.apache.zookeeper.Watcher;
@@ -225,6 +226,102 @@ void testLeaderLosesLeadershipAndReElected() throws Exception {
createGatewayForServer(leader).metadata(new MetadataRequest()).get();
}
+ /**
+ * A single-coordinator cluster must regain leadership without a restart of the process after a
+ * ZooKeeper session expiration, even when the now-empty election parent node has been deleted.
+ *
+ * The election parent {@code /coordinators/election} is created as a container znode. When
+ * the session expires, ZooKeeper deletes the ephemeral election node, and the empty container
+ * parent can then be garbage-collected by ZooKeeper while the coordinator is still recovering.
+ * This is simulated here by deleting the parent right after the session expiration.
+ */
+ @Test
+ void testRegainsLeadershipAfterSessionExpirationWithElectionParentDeleted() throws Exception {
+ // single coordinator: no standby can take over
+ coordinatorServer1 = new CoordinatorServer(createConfiguration());
+ coordinatorServer1.start();
+ waitUntilCoordinatorServerElected();
+
+ String electionPath = ZkData.CoordinatorElectionZNode.path();
+ List