From ccad7a776c1eb935377d8811f2d81728f0f6791c Mon Sep 17 00:00:00 2001 From: Matteo Merli Date: Mon, 2 Nov 2026 15:29:04 -0800 Subject: [PATCH 1/2] [fix][meta] Fix leader elections settling on a lock deleted while they read or write it LeaderElectionImpl.handlePathNotification() ignored the notifications unless the election had settled (LeaderIsPresent), while an election cycle reads the lock, and may write it, as ElectionInProgress. When the store notified the deletion of the lock before it delivered the result of a read or a write that came before the deletion, the election dropped the deletion and settled on what was gone: - A candidate that read the lock of a leader that then went away followed it. It kept reporting that leader, and nothing made it elect again: Created notifications are ignored, and only a Reconnected or SessionReestablished event revalidates. With no other candidate, nobody led. - An election that wrote the lock, which was then deleted, led without it. ZooKeeper isn't exposed: ZKMetadataStore completes the operations and dispatches the watch events on its single event thread, in order. Oxia is: OxiaMetadataStore completes them on the Oxia client's threads, and the notifications come on a separate stream dispatched on the event thread. The election now counts the deletions of the lock it's notified of. A cycle takes the count before it reads the lock and before it writes it, and reads the lock again instead of settling if the count changed. --- .../coordination/impl/LeaderElectionImpl.java | 33 +++++- .../impl/LeaderElectionImplTest.java | 106 ++++++++++++++++++ 2 files changed, 135 insertions(+), 4 deletions(-) diff --git a/pulsar-metadata/src/main/java/org/apache/pulsar/metadata/coordination/impl/LeaderElectionImpl.java b/pulsar-metadata/src/main/java/org/apache/pulsar/metadata/coordination/impl/LeaderElectionImpl.java index 3ffb778f4d1cd..c2914d2dd589f 100644 --- a/pulsar-metadata/src/main/java/org/apache/pulsar/metadata/coordination/impl/LeaderElectionImpl.java +++ b/pulsar-metadata/src/main/java/org/apache/pulsar/metadata/coordination/impl/LeaderElectionImpl.java @@ -74,6 +74,12 @@ private enum InternalState { private InternalState internalState; + // Counts the deletions of the lock this election was notified of. A deletion can be notified before the result of + // a read or a write of the lock that came before it (Oxia delivers them on different threads), so an election + // cycle that sees the count change while it reads or writes the lock reads it again instead of settling on what + // may be gone. + private long lockDeletions; + private static final int LEADER_ELECTION_RETRY_DELAY_SECONDS = 5; // Upper bound for getLeaderValue() waiting on an election that never settles, aligned with the @@ -150,21 +156,29 @@ private synchronized CompletableFuture elect() { } // First check if there's already a leader elected internalState = InternalState.ElectionInProgress; + long lockDeletionsBeforeRead = lockDeletions; return store.get(path).thenCompose(optLock -> { if (optLock.isPresent()) { - return handleExistingLeaderValue(optLock.get()); + return handleExistingLeaderValue(optLock.get(), lockDeletionsBeforeRead); } else { return tryToBecomeLeader(); } }); } - private synchronized CompletableFuture handleExistingLeaderValue(GetResult res) { + private synchronized CompletableFuture handleExistingLeaderValue(GetResult res, + long lockDeletionsBeforeRead) { if (internalState == InternalState.Closed) { // Closed while the lock was being read: the value read must not bring the election back, nor make it // delete the lock return FutureUtil.failedFuture(new AlreadyClosedException("The leader election was already closed")); } + if (lockDeletions != lockDeletionsBeforeRead) { + // The lock may have been deleted after it was read: the leader it names may be gone + log.debug().attr("path", path) + .log("A deletion of the lock was notified while reading it. Reading it again."); + return elect(); + } T existingValue; try { existingValue = serde.deserialize(path, res.getValue(), res.getStat()); @@ -236,10 +250,17 @@ private synchronized CompletableFuture tryToBecomeLeader() } CompletableFuture result = new CompletableFuture<>(); + long lockDeletionsBeforeWrite = lockDeletions; store.put(path, payload, Optional.of(-1L), EnumSet.of(CreateOption.Ephemeral)) .thenAccept(stat -> { synchronized (LeaderElectionImpl.this) { - if (internalState == InternalState.ElectionInProgress) { + if (internalState == InternalState.ElectionInProgress + && lockDeletions != lockDeletionsBeforeWrite) { + // The lock may have been deleted once written: read it again rather than lead without it + log.debug().attr("path", path).attr("value", value) + .log("A deletion of the lock was notified while taking it. Reading it again."); + FutureUtil.completeAfter(result, elect()); + } else if (internalState == InternalState.ElectionInProgress) { log.info().attr("path", path).attr("value", value) .log("Acquired leadership"); internalState = InternalState.LeaderIsPresent; @@ -448,8 +469,12 @@ void handleSessionNotification(SessionEvent event) { // Called for the notifications on the path of this election void handlePathNotification(Notification notification) { synchronized (this) { + if (notification.getType() == NotificationType.Deleted) { + lockDeletions++; + } if (internalState != InternalState.LeaderIsPresent) { - // Ignoring notification since we're not trying to become leader + // Not taking part in the election, or an election cycle is in progress: it checks the deletions once + // it has read or written the lock return; } diff --git a/pulsar-metadata/src/test/java/org/apache/pulsar/metadata/coordination/impl/LeaderElectionImplTest.java b/pulsar-metadata/src/test/java/org/apache/pulsar/metadata/coordination/impl/LeaderElectionImplTest.java index 0a64206023d5a..b8041fded9e33 100644 --- a/pulsar-metadata/src/test/java/org/apache/pulsar/metadata/coordination/impl/LeaderElectionImplTest.java +++ b/pulsar-metadata/src/test/java/org/apache/pulsar/metadata/coordination/impl/LeaderElectionImplTest.java @@ -50,6 +50,7 @@ import org.apache.pulsar.metadata.api.Option; import org.apache.pulsar.metadata.api.Stat; import org.apache.pulsar.metadata.api.coordination.CoordinationService; +import org.apache.pulsar.metadata.api.coordination.LeaderElection; import org.apache.pulsar.metadata.api.coordination.LeaderElectionState; import org.apache.pulsar.metadata.api.extended.MetadataStoreExtended; import org.apache.pulsar.metadata.api.extended.SessionEvent; @@ -232,4 +233,109 @@ public CompletableFuture> get(String p, Set