This is an automated email from the ASF dual-hosted git repository.

amaechler pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new c55da2ae85f fix: re-announce a restarted service once old ZooKeeper 
entry expires (#20476)
c55da2ae85f is described below

commit c55da2ae85ff46dff067808aa3bda9cfa82f0337
Author: Andreas Maechler <[email protected]>
AuthorDate: Tue Oct 20 08:59:39 2026 -0600

    fix: re-announce a restarted service once old ZooKeeper entry expires 
(#20476)
    
    * Re-create announcements blocked by a stale ephemeral node
    
    When a process restarts within the ZooKeeper session timeout, the ephemeral
    node of its previous session still exists at the announcement path, so the
    announcer's create fails with NODEEXISTS. Both announcers relied on their
    cache to report the removal of that node, but PathChildrenCache and
    CuratorCache only report removals of nodes they have already loaded. If
    ZooKeeper deletes the stale node before the cache's initial load sees it,
    the announcement is never re-created and the process stays unregistered
    until it restarts.
    
    Handle NODEEXISTS in a create callback: set an exists watch on the path and
    re-create the announcement once the node is gone, as Curator's
    PersistentNode does. All re-create paths, including the existing cache
    listeners and the reconnect handler, now go through reinstateAnnouncement,
    which uses the current payload and skips paths that were unannounced in the
    meantime. Creates and exists checks that fail for any other reason are
    logged as warnings instead of being dropped.
    
    The existing tests waited for CuratorListener CREATE events, which Curator
    does not deliver once a create has a callback, so they now poll until the
    node exists and belongs to the announcer's session, using helpers in
    CuratorTestBase. New tests reproduce the race deterministically, both when
    the stale node is removed before the exists watch is set and after it.
    
    * Re-arm stale-node watch after reconnect, guard reinstate vs unannounce
    
    NodeAnnouncer had no reconnect handling, so if its own session expired
    while it waited for a stale node to go away, the exists watch was lost
    and nothing re-created the announcement unless the cache happened to
    hold the stale node. Re-announce every path on RECONNECTED: the create
    hits NODEEXISTS again and watches the node from the new session.
    
    Both announcers read the payload and then queued the create, so an
    unannounce running in between could leave a node behind. Queue the
    create inside computeIfPresent on the announcement's entry: a concurrent
    unannounce now either removes the entry first or waits, and its delete
    follows the create.
    
    createAnnouncement now wraps Curator's checked exception itself, which
    removes two try/catch blocks.
    
    * Drop unused throws clause from PathChildrenAnnouncer listener
---
 .../druid/curator/announcement/NodeAnnouncer.java  |  94 ++++++++++---
 .../announcement/PathChildrenAnnouncer.java        |  86 +++++++++---
 .../org/apache/druid/curator/CuratorTestBase.java  |  74 +++++++++-
 .../curator/announcement/NodeAnnouncerTest.java    | 152 ++++++++++++++++-----
 .../announcement/PathChildrenAnnouncerTest.java    | 122 +++++++++++------
 5 files changed, 418 insertions(+), 110 deletions(-)

diff --git 
a/server/src/main/java/org/apache/druid/curator/announcement/NodeAnnouncer.java 
b/server/src/main/java/org/apache/druid/curator/announcement/NodeAnnouncer.java
index d38bf79fa38..a5270d5b89e 100644
--- 
a/server/src/main/java/org/apache/druid/curator/announcement/NodeAnnouncer.java
+++ 
b/server/src/main/java/org/apache/druid/curator/announcement/NodeAnnouncer.java
@@ -22,10 +22,13 @@ package org.apache.druid.curator.announcement;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.errorprone.annotations.concurrent.GuardedBy;
 import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.api.CuratorWatcher;
 import org.apache.curator.framework.api.transaction.CuratorMultiTransaction;
 import org.apache.curator.framework.api.transaction.CuratorOp;
 import org.apache.curator.framework.recipes.cache.CuratorCache;
 import org.apache.curator.framework.recipes.cache.CuratorCacheListener;
+import org.apache.curator.framework.state.ConnectionState;
+import org.apache.curator.framework.state.ConnectionStateListener;
 import org.apache.curator.utils.ZKPaths;
 import org.apache.druid.java.util.common.IAE;
 import org.apache.druid.java.util.common.ISE;
@@ -35,6 +38,7 @@ import org.apache.druid.java.util.common.logger.Logger;
 import org.apache.druid.utils.CloseableUtils;
 import org.apache.zookeeper.CreateMode;
 import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.Watcher;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -94,6 +98,26 @@ public class NodeAnnouncer implements ServiceAnnouncer
   @GuardedBy("toAnnounce")
   private final List<String> parentsIBuilt = new CopyOnWriteArrayList<>();
 
+  /**
+   * Watches a node that already existed when we tried to create our 
announcement, see {@link #watchExistingNode}.
+   */
+  private final CuratorWatcher existingNodeWatcher = event -> {
+    if (event.getType() == Watcher.Event.EventType.NodeDeleted) {
+      reinstateAnnouncement(event.getPath());
+    } else if (event.getType() == Watcher.Event.EventType.NodeDataChanged) {
+      watchExistingNode(event.getPath());
+    }
+  };
+
+  /**
+   * Re-announces every path after a reconnect, since watches set on an 
expired session are gone.
+   */
+  private final ConnectionStateListener reconnectListener = (client, newState) 
-> {
+    if (newState == ConnectionState.RECONNECTED) {
+      announcedPaths.keySet().forEach(this::reinstateAnnouncement);
+    }
+  };
+
   public NodeAnnouncer(CuratorFramework curator, ExecutorService exec)
   {
     this.curator = curator;
@@ -118,6 +142,7 @@ public class NodeAnnouncer implements ServiceAnnouncer
       }
 
       started = true;
+      curator.getConnectionStateListenable().addListener(reconnectListener);
 
       for (Announceable announceable : toAnnounce) {
         announce(announceable.path, announceable.bytes, 
announceable.removeParentsIfCreated);
@@ -150,6 +175,7 @@ public class NodeAnnouncer implements ServiceAnnouncer
   @GuardedBy("toAnnounce")
   private void closeResources()
   {
+    curator.getConnectionStateListenable().removeListener(reconnectListener);
     try {
       // Close all caches...
       CloseableUtils.closeAll(listeners.values());
@@ -263,12 +289,7 @@ public class NodeAnnouncer implements ServiceAnnouncer
     }
 
     if (created) {
-      try {
-        createAnnouncement(path, bytes);
-      }
-      catch (Exception e) {
-        throw new RuntimeException(e);
-      }
+      createAnnouncement(path, bytes);
     }
   }
 
@@ -280,16 +301,7 @@ public class NodeAnnouncer implements ServiceAnnouncer
     cache.listenable().addListener(
         (type, oldData, data) -> {
           if (type == CuratorCacheListener.Type.NODE_DELETED) {
-            final byte[] previouslyAnnouncedData = announcedPaths.get(path);
-            if (previouslyAnnouncedData != null) {
-              try {
-                log.info("ZooKeeper Node[%s] dropped, reinstating...", path);
-                createAnnouncement(path, previouslyAnnouncedData);
-              }
-              catch (Exception e) {
-                throw new RuntimeException(e);
-              }
-            }
+            reinstateAnnouncement(path);
           }
         }, nodeCacheExecutor
     );
@@ -325,9 +337,55 @@ public class NodeAnnouncer implements ServiceAnnouncer
     }
   }
 
-  private void createAnnouncement(final String path, byte[] value) throws 
Exception
+  private void createAnnouncement(final String path, byte[] value)
+  {
+    try {
+      
curator.create().compressed().withMode(CreateMode.EPHEMERAL).inBackground(
+          (client, event) -> {
+            if (event.getResultCode() == 
KeeperException.Code.NODEEXISTS.intValue()) {
+              watchExistingNode(path);
+            } else if (event.getResultCode() != 
KeeperException.Code.OK.intValue()) {
+              log.warn("Failed to announce node[%s]: %s", path, 
KeeperException.Code.get(event.getResultCode()));
+            }
+          }
+      ).forPath(path, value);
+    }
+    catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  /**
+   * Watches a node that was already present when we tried to create our 
announcement at its path, for example one
+   * left behind by a previous session of this process. The announcement is 
re-created once that node is removed,
+   * even if the {@link CuratorCache} never saw it.
+   */
+  private void watchExistingNode(final String path) throws Exception
+  {
+    curator.checkExists().usingWatcher(existingNodeWatcher).inBackground(
+        (client, event) -> {
+          if (event.getResultCode() == KeeperException.Code.NONODE.intValue()) 
{
+            reinstateAnnouncement(path);
+          } else if (event.getResultCode() == 
KeeperException.Code.OK.intValue()) {
+            final long owner = event.getStat().getEphemeralOwner();
+            if (owner != 
curator.getZookeeperClient().getZooKeeper().getSessionId()) {
+              log.info("Node[%s] already exists with session[0x%x], will 
reinstate once it is removed.", path, owner);
+            }
+          } else {
+            log.warn("Failed to watch existing node[%s]: %s", path, 
KeeperException.Code.get(event.getResultCode()));
+          }
+        }
+    ).forPath(path);
+  }
+
+  private void reinstateAnnouncement(final String path)
   {
-    
curator.create().compressed().withMode(CreateMode.EPHEMERAL).inBackground().forPath(path,
 value);
+    // Create while holding the entry, so a concurrent unannounce either 
prevents it or deletes the node afterwards.
+    announcedPaths.computeIfPresent(path, (p, value) -> {
+      log.info("ZooKeeper Node[%s] dropped, reinstating...", path);
+      createAnnouncement(path, value);
+      return value;
+    });
   }
 
   private void updateAnnouncement(final String path, final byte[] value) 
throws Exception
diff --git 
a/server/src/main/java/org/apache/druid/curator/announcement/PathChildrenAnnouncer.java
 
b/server/src/main/java/org/apache/druid/curator/announcement/PathChildrenAnnouncer.java
index ec754d96be2..ad9f1073c80 100644
--- 
a/server/src/main/java/org/apache/druid/curator/announcement/PathChildrenAnnouncer.java
+++ 
b/server/src/main/java/org/apache/druid/curator/announcement/PathChildrenAnnouncer.java
@@ -22,9 +22,9 @@ package org.apache.druid.curator.announcement;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.errorprone.annotations.concurrent.GuardedBy;
 import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.api.CuratorWatcher;
 import org.apache.curator.framework.api.transaction.CuratorMultiTransaction;
 import org.apache.curator.framework.api.transaction.CuratorOp;
-import org.apache.curator.framework.recipes.cache.ChildData;
 import org.apache.curator.framework.recipes.cache.PathChildrenCache;
 import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
 import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
@@ -38,6 +38,7 @@ import org.apache.druid.java.util.common.logger.Logger;
 import org.apache.druid.utils.CloseableUtils;
 import org.apache.zookeeper.CreateMode;
 import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.Watcher;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -84,6 +85,17 @@ public class PathChildrenAnnouncer implements 
ServiceAnnouncer
 
   private boolean started = false;
 
+  /**
+   * Watches a node that already existed when we tried to create our 
announcement, see {@link #watchExistingNode}.
+   */
+  private final CuratorWatcher existingNodeWatcher = event -> {
+    if (event.getType() == Watcher.Event.EventType.NodeDeleted) {
+      reinstateAnnouncement(event.getPath());
+    } else if (event.getType() == Watcher.Event.EventType.NodeDataChanged) {
+      watchExistingNode(event.getPath());
+    }
+  };
+
   public PathChildrenAnnouncer(
       CuratorFramework curator,
       ExecutorService exec
@@ -252,20 +264,14 @@ public class PathChildrenAnnouncer implements 
ServiceAnnouncer
                 private final AtomicReference<Set<String>> pathsLost = new 
AtomicReference<>(null);
 
                 @Override
-                public void childEvent(CuratorFramework client, 
PathChildrenCacheEvent event) throws Exception
+                public void childEvent(CuratorFramework client, 
PathChildrenCacheEvent event)
                 {
                   // NOTE: ZooKeeper does not guarantee that we will get every 
event, and thus PathChildrenCache doesn't
                   // as well. If one of the below events are missed, Announcer 
might not work properly.
                   log.debug("Path[%s] got event[%s]", parentPath, event);
                   switch (event.getType()) {
                     case CHILD_REMOVED:
-                      final ChildData child = event.getData();
-                      final ZKPaths.PathAndNode childPath = 
ZKPaths.getPathAndNode(child.getPath());
-                      final byte[] value = 
finalSubPaths.get(childPath.getNode());
-                      if (value != null) {
-                        log.info("Node[%s] dropped, reinstating.", 
child.getPath());
-                        createAnnouncement(child.getPath(), value);
-                      }
+                      reinstateAnnouncement(event.getData().getPath());
                       break;
                     case CONNECTION_LOST:
                       // Lost connection, which means session is broken, take 
inventory of what has been seen.
@@ -292,9 +298,7 @@ public class PathChildrenAnnouncer implements 
ServiceAnnouncer
 
                       if (thePathsLost != null) {
                         for (String path : thePathsLost) {
-                          log.info("Reinstating [%s]", path);
-                          final ZKPaths.PathAndNode split = 
ZKPaths.getPathAndNode(path);
-                          createAnnouncement(path, 
announcements.get(split.getPath()).get(split.getNode()));
+                          reinstateAnnouncement(path);
                         }
                       }
                       break;
@@ -341,12 +345,7 @@ public class PathChildrenAnnouncer implements 
ServiceAnnouncer
     }
 
     if (created) {
-      try {
-        createAnnouncement(path, bytes);
-      }
-      catch (Exception e) {
-        throw new RuntimeException(e);
-      }
+      createAnnouncement(path, bytes);
     }
   }
 
@@ -386,9 +385,56 @@ public class PathChildrenAnnouncer implements 
ServiceAnnouncer
     }
   }
 
-  private void createAnnouncement(final String path, byte[] value) throws 
Exception
+  private void createAnnouncement(final String path, byte[] value)
   {
-    
curator.create().compressed().withMode(CreateMode.EPHEMERAL).inBackground().forPath(path,
 value);
+    try {
+      
curator.create().compressed().withMode(CreateMode.EPHEMERAL).inBackground(
+          (client, event) -> {
+            if (event.getResultCode() == 
KeeperException.Code.NODEEXISTS.intValue()) {
+              watchExistingNode(path);
+            } else if (event.getResultCode() != 
KeeperException.Code.OK.intValue()) {
+              log.warn("Failed to announce node[%s]: %s", path, 
KeeperException.Code.get(event.getResultCode()));
+            }
+          }
+      ).forPath(path, value);
+    }
+    catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  /**
+   * Watches a node that was already present when we tried to create our 
announcement at its path, for example one
+   * left behind by a previous session of this process. The announcement is 
re-created once that node is removed,
+   * even if the {@link PathChildrenCache} never saw it.
+   */
+  private void watchExistingNode(final String path) throws Exception
+  {
+    curator.checkExists().usingWatcher(existingNodeWatcher).inBackground(
+        (client, event) -> {
+          if (event.getResultCode() == KeeperException.Code.NONODE.intValue()) 
{
+            reinstateAnnouncement(path);
+          } else if (event.getResultCode() == 
KeeperException.Code.OK.intValue()) {
+            final long owner = event.getStat().getEphemeralOwner();
+            if (owner != 
curator.getZookeeperClient().getZooKeeper().getSessionId()) {
+              log.info("Node[%s] already exists with session[0x%x], will 
reinstate once it is removed.", path, owner);
+            }
+          } else {
+            log.warn("Failed to watch existing node[%s]: %s", path, 
KeeperException.Code.get(event.getResultCode()));
+          }
+        }
+    ).forPath(path);
+  }
+
+  private void reinstateAnnouncement(final String path)
+  {
+    final ZKPaths.PathAndNode pathAndNode = ZKPaths.getPathAndNode(path);
+    // Create while holding the entry, so a concurrent unannounce either 
prevents it or deletes the node afterwards.
+    
announcements.get(pathAndNode.getPath()).computeIfPresent(pathAndNode.getNode(),
 (node, value) -> {
+      log.info("Node[%s] dropped, reinstating.", path);
+      createAnnouncement(path, value);
+      return value;
+    });
   }
 
   private void updateAnnouncement(final String path, final byte[] value) 
throws Exception
diff --git a/server/src/test/java/org/apache/druid/curator/CuratorTestBase.java 
b/server/src/test/java/org/apache/druid/curator/CuratorTestBase.java
index c58999565d7..a481fa3e059 100644
--- a/server/src/test/java/org/apache/druid/curator/CuratorTestBase.java
+++ b/server/src/test/java/org/apache/druid/curator/CuratorTestBase.java
@@ -24,8 +24,15 @@ import org.apache.curator.framework.CuratorFrameworkFactory;
 import org.apache.curator.retry.RetryOneTime;
 import org.apache.curator.test.TestingServer;
 import org.apache.curator.test.Timing;
+import org.apache.zookeeper.CreateMode;
+import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.data.Stat;
+import org.junit.jupiter.api.Assertions;
 
 import java.io.IOException;
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 /**
  */
@@ -39,7 +46,12 @@ public class CuratorTestBase
   {
     server = new TestingServer();
     timing = new Timing();
-    curator = CuratorFrameworkFactory
+    curator = createCurator();
+  }
+
+  private CuratorFramework createCurator()
+  {
+    return CuratorFrameworkFactory
         .builder()
         .connectString(server.getConnectString())
         .sessionTimeoutMs(timing.session())
@@ -64,4 +76,64 @@ public class CuratorTestBase
   {
     return server.getConnectString();
   }
+
+  /**
+   * Starts a new client, with its own session, that owns an ephemeral node at 
the given path.
+   */
+  public CuratorFramework createEphemeralNodeInNewSession(final String path, 
final byte[] bytes) throws Exception
+  {
+    final CuratorFramework client = createCurator();
+    try {
+      client.start();
+      client.blockUntilConnected();
+      
client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(path,
 bytes);
+      return client;
+    }
+    catch (Exception e) {
+      client.close();
+      throw e;
+    }
+  }
+
+  /**
+   * Waits until the node at the given path holds the given bytes and belongs 
to the session of {@link #curator}.
+   */
+  public void awaitAnnounced(final String path, final byte[] bytes) throws 
Exception
+  {
+    final long deadline = System.currentTimeMillis() + 
timing.forWaiting().milliseconds();
+    while (!isAnnounced(path, bytes)) {
+      Assertions.assertTrue(System.currentTimeMillis() < deadline, "Timed out 
waiting for " + path);
+      Thread.sleep(100);
+    }
+  }
+
+  private boolean isAnnounced(final String path, final byte[] bytes) throws 
Exception
+  {
+    final Stat stat = new Stat();
+    try {
+      final byte[] data = 
curator.getData().decompressed().storingStatIn(stat).forPath(path);
+      return Arrays.equals(bytes, data)
+             && stat.getEphemeralOwner() == 
curator.getZookeeperClient().getZooKeeper().getSessionId();
+    }
+    catch (KeeperException e) {
+      // Missing node, or the client is reconnecting.
+      return false;
+    }
+  }
+
+  /**
+   * Blocks the ZooKeeper event thread of {@link #curator}, which delays all 
of its background callbacks and watch
+   * notifications, until the returned latch is counted down.
+   */
+  public CountDownLatch blockEventThread() throws Exception
+  {
+    final CountDownLatch blocked = new CountDownLatch(1);
+    final CountDownLatch release = new CountDownLatch(1);
+    curator.checkExists().inBackground((client, event) -> {
+      blocked.countDown();
+      release.await(timing.forWaiting().milliseconds(), TimeUnit.MILLISECONDS);
+    }).forPath("/");
+    Assertions.assertTrue(timing.awaitLatch(blocked));
+    return release;
+  }
 }
diff --git 
a/server/src/test/java/org/apache/druid/curator/announcement/NodeAnnouncerTest.java
 
b/server/src/test/java/org/apache/druid/curator/announcement/NodeAnnouncerTest.java
index 6841f87b314..578536a311e 100644
--- 
a/server/src/test/java/org/apache/druid/curator/announcement/NodeAnnouncerTest.java
+++ 
b/server/src/test/java/org/apache/druid/curator/announcement/NodeAnnouncerTest.java
@@ -19,7 +19,7 @@
 
 package org.apache.druid.curator.announcement;
 
-import org.apache.curator.framework.api.CuratorEventType;
+import org.apache.curator.framework.CuratorFramework;
 import org.apache.curator.framework.api.transaction.CuratorOp;
 import org.apache.curator.framework.api.transaction.CuratorTransactionResult;
 import org.apache.curator.test.KillSession;
@@ -29,6 +29,7 @@ import org.apache.druid.java.util.common.IAE;
 import org.apache.druid.java.util.common.ISE;
 import org.apache.druid.java.util.common.StringUtils;
 import org.apache.druid.java.util.common.concurrent.Execs;
+import org.apache.druid.testing.junit.LoggerCaptureExtension;
 import org.apache.zookeeper.KeeperException.Code;
 import org.apache.zookeeper.data.Stat;
 import org.junit.jupiter.api.AfterEach;
@@ -36,14 +37,18 @@ import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Timeout;
+import org.junit.jupiter.api.extension.RegisterExtension;
 
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
 
 public class NodeAnnouncerTest extends CuratorTestBase
 {
+  @RegisterExtension
+  public final LoggerCaptureExtension announcerLogs = new 
LoggerCaptureExtension(NodeAnnouncer.class);
+
   private ExecutorService exec;
 
   @BeforeEach
@@ -194,19 +199,13 @@ public class NodeAnnouncerTest extends CuratorTestBase
       Assertions.assertArrayEquals(billy, 
curator.getData().decompressed().forPath(testPath1), "/test1 still has data");
       Assertions.assertArrayEquals(billy, 
curator.getData().decompressed().forPath(testPath2), "/somewhere/test2 has 
data");
 
-      final CountDownLatch latch = new CountDownLatch(1);
-      curator.getCuratorListenable().addListener((client, event) -> {
-        if (event.getType() == CuratorEventType.CREATE && 
event.getPath().equals(testPath1)) {
-          latch.countDown();
-        }
-      });
       final CuratorOp deleteOp = 
curator.transactionOp().delete().forPath(testPath1);
       final Collection<CuratorTransactionResult> results = 
curator.transaction().forOperations(deleteOp);
       Assertions.assertEquals(1, results.size(), "Expected one result from the 
delete op");
       final CuratorTransactionResult result = results.iterator().next();
       Assertions.assertEquals(Code.OK.intValue(), result.getError(), "Expected 
OK code on delete");
 
-      Assertions.assertTrue(timing.forWaiting().awaitLatch(latch), "Wait for 
/test1 to be recreated");
+      awaitAnnounced(testPath1, billy);
 
       Assertions.assertArrayEquals(billy, 
curator.getData().decompressed().forPath(testPath1), "Expected /test1 data to 
be restored");
       Assertions.assertArrayEquals(billy, 
curator.getData().decompressed().forPath(testPath2), "Expected /somewhere/test2 
data to remain");
@@ -236,17 +235,16 @@ public class NodeAnnouncerTest extends CuratorTestBase
       final byte[] billy = StringUtils.toUtf8("billy");
       final String testPath1 = "/test1";
       final String testPath2 = "/somewhere/test2";
-      final String[] paths = new String[]{testPath1, testPath2};
       announcer.announce(testPath1, billy);
       announcer.announce(testPath2, billy);
 
       Assertions.assertArrayEquals(billy, 
curator.getData().decompressed().forPath(testPath1));
       Assertions.assertArrayEquals(billy, 
curator.getData().decompressed().forPath(testPath2));
 
-      final CountDownLatch latch = createCountdownLatchForPaths(paths);
       KillSession.kill(curator.getZookeeperClient().getZooKeeper(), 
server.getConnectString());
 
-      Assertions.assertTrue(timing.forWaiting().awaitLatch(latch), "Await 
latch after killing session");
+      awaitAnnounced(testPath1, billy);
+      awaitAnnounced(testPath2, billy);
 
       Assertions.assertArrayEquals(billy, 
curator.getData().decompressed().forPath(testPath1));
       Assertions.assertArrayEquals(billy, 
curator.getData().decompressed().forPath(testPath2));
@@ -279,7 +277,9 @@ public class NodeAnnouncerTest extends CuratorTestBase
     try {
       Assertions.assertNull(curator.checkExists().forPath(parent));
 
-      awaitAnnounce(announcer, testPath, billy, true);
+      announcer.announce(testPath, billy, true);
+
+      awaitAnnounced(testPath, billy);
 
       Assertions.assertNotNull(curator.checkExists().forPath(parent));
     }
@@ -306,7 +306,9 @@ public class NodeAnnouncerTest extends CuratorTestBase
     try {
       Assertions.assertEquals(initialStat.getMzxid(), 
curator.checkExists().forPath(parent).getMzxid());
 
-      awaitAnnounce(announcer, testPath, billy, true);
+      announcer.announce(testPath, billy, true);
+
+      awaitAnnounced(testPath, billy);
 
       Assertions.assertEquals(initialStat.getMzxid(), 
curator.checkExists().forPath(parent).getMzxid());
     }
@@ -330,7 +332,9 @@ public class NodeAnnouncerTest extends CuratorTestBase
     try {
       Assertions.assertNull(curator.checkExists().forPath(parent));
 
-      awaitAnnounce(announcer, testPath, billy, false);
+      announcer.announce(testPath, billy, false);
+
+      awaitAnnounced(testPath, billy);
 
       Assertions.assertNotNull(curator.checkExists().forPath(parent));
     }
@@ -341,27 +345,111 @@ public class NodeAnnouncerTest extends CuratorTestBase
     Assertions.assertNotNull(curator.checkExists().forPath(parent));
   }
 
-  private void awaitAnnounce(
-          final NodeAnnouncer announcer,
-          final String path,
-          final byte[] bytes,
-          boolean removeParentsIfCreated
-  ) throws InterruptedException
+  /**
+   * A restarted process announces a path whose node still belongs to its 
previous session, and that session expires
+   * before the announcer's cache has seen the node.
+   */
+  @Test
+  @Timeout(value = 60_000L, unit = TimeUnit.MILLISECONDS)
+  public void testReinstatesWhenStaleNodeIsRemovedBeforeCacheSeesIt() throws 
Exception
   {
-    final CountDownLatch latch = createCountdownLatchForPaths(path);
-    announcer.announce(path, bytes, removeParentsIfCreated);
-    latch.await();
+    final byte[] billy = StringUtils.toUtf8("billy");
+    final String testPath = "/somewhere/test";
+    final NodeAnnouncer announcer = new NodeAnnouncer(curator, exec);
+
+    try (CuratorFramework staleCurator = 
createEphemeralNodeInNewSession(testPath, StringUtils.toUtf8("stale"))) {
+      final long staleSessionId = 
staleCurator.getZookeeperClient().getZooKeeper().getSessionId();
+
+      final CountDownLatch release = blockEventThread();
+      try {
+        announcer.start();
+        announcer.announce(testPath, billy);
+
+        // Requests of a session are processed in order, so the announcer's 
create has already failed with NODEEXISTS.
+        Assertions.assertEquals(staleSessionId, 
curator.checkExists().forPath(testPath).getEphemeralOwner());
+
+        staleCurator.close();
+        Assertions.assertNull(curator.checkExists().forPath(testPath));
+      }
+      finally {
+        release.countDown();
+      }
+
+      awaitAnnounced(testPath, billy);
+    }
+    finally {
+      announcer.stop();
+    }
   }
 
-  private CountDownLatch createCountdownLatchForPaths(String... paths)
+  /**
+   * The stale node is removed only after the announcer started watching it, 
while the announcer's cache cannot react
+   * because its executor is busy.
+   */
+  @Test
+  @Timeout(value = 60_000L, unit = TimeUnit.MILLISECONDS)
+  public void testReinstatesWhenWatchedStaleNodeIsRemoved() throws Exception
   {
-    final CountDownLatch latch = new CountDownLatch(paths.length);
-    curator.getCuratorListenable().addListener((client, event) -> {
-      if (event.getType() == CuratorEventType.CREATE && 
Arrays.asList(paths).contains(event.getPath())) {
-        latch.countDown();
-      }
-    });
+    final byte[] billy = StringUtils.toUtf8("billy");
+    final String testPath = "/somewhere/test";
+    final NodeAnnouncer announcer = new NodeAnnouncer(curator, exec);
+    final CountDownLatch releaseExec = new CountDownLatch(1);
+
+    try (CuratorFramework staleCurator = 
createEphemeralNodeInNewSession(testPath, StringUtils.toUtf8("stale"))) {
+      exec.submit(() -> releaseExec.await(1, TimeUnit.MINUTES));
+      announcer.start();
+      announcer.announce(testPath, billy);
 
-    return latch;
+      // Logged once the exists watch on the stale node is set.
+      awaitLogEvents("already exists", 1);
+
+      staleCurator.close();
+      awaitAnnounced(testPath, billy);
+    }
+    finally {
+      releaseExec.countDown();
+      announcer.stop();
+    }
+  }
+
+  /**
+   * The announcer's own session expires while it waits for the stale node to 
go away, which drops its watch.
+   */
+  @Test
+  @Timeout(value = 60_000L, unit = TimeUnit.MILLISECONDS)
+  public void testReinstatesWhenOwnSessionExpiresWhileWatchingStaleNode() 
throws Exception
+  {
+    final byte[] billy = StringUtils.toUtf8("billy");
+    final String testPath = "/somewhere/test";
+    final NodeAnnouncer announcer = new NodeAnnouncer(curator, exec);
+    final CountDownLatch releaseExec = new CountDownLatch(1);
+
+    try (CuratorFramework staleCurator = 
createEphemeralNodeInNewSession(testPath, StringUtils.toUtf8("stale"))) {
+      exec.submit(() -> releaseExec.await(1, TimeUnit.MINUTES));
+      announcer.start();
+      announcer.announce(testPath, billy);
+      awaitLogEvents("already exists", 1);
+
+      KillSession.kill(curator.getZookeeperClient().getZooKeeper());
+      // After reconnecting, the announcer finds the stale node again and 
watches it from the new session.
+      awaitLogEvents("already exists", 2);
+
+      staleCurator.close();
+      awaitAnnounced(testPath, billy);
+    }
+    finally {
+      releaseExec.countDown();
+      announcer.stop();
+    }
+  }
+
+  private void awaitLogEvents(final String text, final long count) throws 
InterruptedException
+  {
+    while (announcerLogs.getLogEvents()
+                        .stream()
+                        .filter(event -> 
event.getMessage().getFormattedMessage().contains(text))
+                        .count() < count) {
+      Thread.sleep(10);
+    }
   }
 }
diff --git 
a/server/src/test/java/org/apache/druid/curator/announcement/PathChildrenAnnouncerTest.java
 
b/server/src/test/java/org/apache/druid/curator/announcement/PathChildrenAnnouncerTest.java
index 54d955b4ce4..408913b0e41 100644
--- 
a/server/src/test/java/org/apache/druid/curator/announcement/PathChildrenAnnouncerTest.java
+++ 
b/server/src/test/java/org/apache/druid/curator/announcement/PathChildrenAnnouncerTest.java
@@ -20,9 +20,6 @@
 package org.apache.druid.curator.announcement;
 
 import org.apache.curator.framework.CuratorFramework;
-import org.apache.curator.framework.api.CuratorEvent;
-import org.apache.curator.framework.api.CuratorEventType;
-import org.apache.curator.framework.api.CuratorListener;
 import org.apache.curator.framework.api.transaction.CuratorOp;
 import org.apache.curator.framework.api.transaction.CuratorTransactionResult;
 import org.apache.curator.test.KillSession;
@@ -31,6 +28,7 @@ import org.apache.druid.curator.CuratorTestBase;
 import org.apache.druid.java.util.common.StringUtils;
 import org.apache.druid.java.util.common.concurrent.Execs;
 import org.apache.druid.java.util.common.logger.Logger;
+import org.apache.druid.testing.junit.LoggerCaptureExtension;
 import org.apache.zookeeper.KeeperException.Code;
 import org.apache.zookeeper.data.Stat;
 import org.junit.jupiter.api.AfterEach;
@@ -38,8 +36,8 @@ import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Timeout;
+import org.junit.jupiter.api.extension.RegisterExtension;
 
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
@@ -51,6 +49,10 @@ import java.util.concurrent.TimeUnit;
 public class PathChildrenAnnouncerTest extends CuratorTestBase
 {
   private static final Logger log = new 
Logger(PathChildrenAnnouncerTest.class);
+
+  @RegisterExtension
+  public final LoggerCaptureExtension announcerLogs = new 
LoggerCaptureExtension(PathChildrenAnnouncer.class);
+
   private ExecutorService exec;
 
   @BeforeEach
@@ -101,15 +103,13 @@ public class PathChildrenAnnouncerTest extends 
CuratorTestBase
           "/somewhere/test2 has data"
       );
 
-      final CountDownLatch latch = createCountdownLatchForPaths(testPath1);
-
       final CuratorOp deleteOp = 
curator.transactionOp().delete().forPath(testPath1);
       final Collection<CuratorTransactionResult> results = 
curator.transaction().forOperations(deleteOp);
       Assertions.assertEquals(1, results.size());
       final CuratorTransactionResult result = results.iterator().next();
       Assertions.assertEquals(Code.OK.intValue(), result.getError()); // 
assert delete
 
-      Assertions.assertTrue(timing.forWaiting().awaitLatch(latch), "Wait for 
/test1 to be created");
+      awaitAnnounced(testPath1, billy);
 
       Assertions.assertArrayEquals(
           billy,
@@ -151,18 +151,16 @@ public class PathChildrenAnnouncerTest extends 
CuratorTestBase
       final byte[] billy = StringUtils.toUtf8("billy");
       final String testPath1 = "/test1";
       final String testPath2 = "/somewhere/test2";
-      final String[] paths = new String[]{testPath1, testPath2};
       announcer.announce(testPath1, billy);
       announcer.announce(testPath2, billy);
 
       Assertions.assertArrayEquals(billy, 
curator.getData().decompressed().forPath(testPath1));
       Assertions.assertArrayEquals(billy, 
curator.getData().decompressed().forPath(testPath2));
 
-      final CountDownLatch latch = createCountdownLatchForPaths(paths);
-
       KillSession.kill(curator.getZookeeperClient().getZooKeeper(), 
server.getConnectString());
 
-      Assertions.assertTrue(timing.forWaiting().awaitLatch(latch));
+      awaitAnnounced(testPath1, billy);
+      awaitAnnounced(testPath2, billy);
 
       Assertions.assertArrayEquals(billy, 
curator.getData().decompressed().forPath(testPath1));
       Assertions.assertArrayEquals(billy, 
curator.getData().decompressed().forPath(testPath2));
@@ -194,7 +192,9 @@ public class PathChildrenAnnouncerTest extends 
CuratorTestBase
     try {
       Assertions.assertNull(curator.checkExists().forPath(parent));
 
-      awaitAnnounce(announcer, testPath, billy, true);
+      announcer.announce(testPath, billy, true);
+
+      awaitAnnounced(testPath, billy);
 
       Assertions.assertNotNull(curator.checkExists().forPath(parent));
     }
@@ -221,7 +221,9 @@ public class PathChildrenAnnouncerTest extends 
CuratorTestBase
     try {
       Assertions.assertEquals(initialStat.getMzxid(), 
curator.checkExists().forPath(parent).getMzxid());
 
-      awaitAnnounce(announcer, testPath, billy, true);
+      announcer.announce(testPath, billy, true);
+
+      awaitAnnounced(testPath, billy);
 
       Assertions.assertEquals(initialStat.getMzxid(), 
curator.checkExists().forPath(parent).getMzxid());
     }
@@ -245,7 +247,9 @@ public class PathChildrenAnnouncerTest extends 
CuratorTestBase
     try {
       Assertions.assertNull(curator.checkExists().forPath(parent));
 
-      awaitAnnounce(announcer, testPath, billy, false);
+      announcer.announce(testPath, billy, false);
+
+      awaitAnnounced(testPath, billy);
 
       Assertions.assertNotNull(curator.checkExists().forPath(parent));
     }
@@ -256,34 +260,74 @@ public class PathChildrenAnnouncerTest extends 
CuratorTestBase
     Assertions.assertNotNull(curator.checkExists().forPath(parent));
   }
 
-  private void awaitAnnounce(
-      final PathChildrenAnnouncer announcer,
-      final String path,
-      final byte[] bytes,
-      boolean removeParentsIfCreated
-  ) throws InterruptedException
+  /**
+   * A restarted process announces a path whose node still belongs to its 
previous session, and that session expires
+   * before the announcer's cache has seen the node.
+   */
+  @Test
+  @Timeout(value = 60_000L, unit = TimeUnit.MILLISECONDS)
+  public void testReinstatesWhenStaleNodeIsRemovedBeforeCacheSeesIt() throws 
Exception
   {
-    CountDownLatch latch = createCountdownLatchForPaths(path);
-    announcer.announce(path, bytes, removeParentsIfCreated);
-    latch.await();
+    final byte[] billy = StringUtils.toUtf8("billy");
+    final String testPath = "/somewhere/test";
+    final PathChildrenAnnouncer announcer = new PathChildrenAnnouncer(curator, 
exec);
+
+    try (CuratorFramework staleCurator = 
createEphemeralNodeInNewSession(testPath, StringUtils.toUtf8("stale"))) {
+      final long staleSessionId = 
staleCurator.getZookeeperClient().getZooKeeper().getSessionId();
+
+      final CountDownLatch release = blockEventThread();
+      try {
+        announcer.start();
+        announcer.announce(testPath, billy);
+
+        // Requests of a session are processed in order, so the announcer's 
create has already failed with NODEEXISTS.
+        Assertions.assertEquals(staleSessionId, 
curator.checkExists().forPath(testPath).getEphemeralOwner());
+
+        staleCurator.close();
+        Assertions.assertNull(curator.checkExists().forPath(testPath));
+      }
+      finally {
+        release.countDown();
+      }
+
+      awaitAnnounced(testPath, billy);
+    }
+    finally {
+      announcer.stop();
+    }
   }
 
-  private CountDownLatch createCountdownLatchForPaths(String... path)
+  /**
+   * The stale node is removed only after the announcer started watching it, 
while the announcer's cache cannot react
+   * because its executor is busy.
+   */
+  @Test
+  @Timeout(value = 60_000L, unit = TimeUnit.MILLISECONDS)
+  public void testReinstatesWhenWatchedStaleNodeIsRemoved() throws Exception
   {
-    final CountDownLatch latch = new CountDownLatch(path.length);
-    curator.getCuratorListenable().addListener(
-        new CuratorListener()
-        {
-          @Override
-          public void eventReceived(CuratorFramework client, CuratorEvent 
event)
-          {
-            if (event.getType() == CuratorEventType.CREATE && 
Arrays.asList(path).contains(event.getPath())) {
-              latch.countDown();
-            }
-          }
-        }
-    );
-
-    return latch;
+    final byte[] billy = StringUtils.toUtf8("billy");
+    final String testPath = "/somewhere/test";
+    final PathChildrenAnnouncer announcer = new PathChildrenAnnouncer(curator, 
exec);
+    final CountDownLatch releaseExec = new CountDownLatch(1);
+
+    try (CuratorFramework staleCurator = 
createEphemeralNodeInNewSession(testPath, StringUtils.toUtf8("stale"))) {
+      exec.submit(() -> releaseExec.await(1, TimeUnit.MINUTES));
+      announcer.start();
+      announcer.announce(testPath, billy);
+
+      // Logged once the exists watch on the stale node is set.
+      while (announcerLogs.getLogEvents()
+                          .stream()
+                          .noneMatch(event -> 
event.getMessage().getFormattedMessage().contains("already exists"))) {
+        Thread.sleep(10);
+      }
+
+      staleCurator.close();
+      awaitAnnounced(testPath, billy);
+    }
+    finally {
+      releaseExec.countDown();
+      announcer.stop();
+    }
   }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to