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

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


The following commit(s) were added to refs/heads/master by this push:
     new 3f631ac86 Fix PersistentTtlNode not deleted if touch node is never 
created (#1260)
3f631ac86 is described below

commit 3f631ac866d0dd119be3724a7c73b4487abc8dfa
Author: chevaris <chevari...@gmail.com>
AuthorDate: Tue Apr 8 14:15:45 2025 +0200

    Fix PersistentTtlNode not deleted if touch node is never created (#1260)
    
    Closes #1258.
    
    See also CURATOR-545, ZOOKEEPER-3546(apache/zookeeper#1138).
---
 .../framework/recipes/nodes/PersistentTtlNode.java | 65 +++++++++++-----------
 .../recipes/nodes/TestPersistentTtlNode.java       | 50 +++++++++++++++++
 2 files changed, 83 insertions(+), 32 deletions(-)

diff --git 
a/curator-recipes/src/main/java/org/apache/curator/framework/recipes/nodes/PersistentTtlNode.java
 
b/curator-recipes/src/main/java/org/apache/curator/framework/recipes/nodes/PersistentTtlNode.java
index 2b2ba561a..33e1608a2 100644
--- 
a/curator-recipes/src/main/java/org/apache/curator/framework/recipes/nodes/PersistentTtlNode.java
+++ 
b/curator-recipes/src/main/java/org/apache/curator/framework/recipes/nodes/PersistentTtlNode.java
@@ -19,6 +19,7 @@
 
 package org.apache.curator.framework.recipes.nodes;
 
+import com.google.common.annotations.VisibleForTesting;
 import java.io.Closeable;
 import java.io.IOException;
 import java.util.Objects;
@@ -37,7 +38,7 @@ import org.slf4j.LoggerFactory;
 
 /**
  * <p>
- *     Manages a {@link PersistentNode} that uses {@link 
CreateMode#CONTAINER}. Asynchronously
+ *     Manages a {@link PersistentNode} that uses {@link 
CreateMode#PERSISTENT_WITH_TTL}. Asynchronously
  *     it creates or updates a child on the persistent node that is marked 
with a provided TTL.
  * </p>
  *
@@ -46,7 +47,7 @@ import org.slf4j.LoggerFactory;
  *     a method of having the parent node deleted if the TTL expires. i.e. if 
the process
  *     that is running the PersistentTtlNode crashes and the TTL elapses, 
first the child node
  *     will be deleted due to the TTL expiration and then the parent node will 
be deleted as it's
- *     a container node with no children.
+ *     a TTL node with no children.
  * </p>
  *
  * <p>
@@ -159,46 +160,46 @@ public class PersistentTtlNode implements Closeable {
         this.client = Objects.requireNonNull(client, "client cannot be null");
         this.ttlMs = ttlMs;
         this.touchScheduleFactor = touchScheduleFactor;
-        node = new PersistentNode(client, CreateMode.CONTAINER, false, path, 
initData, useParentCreation) {
-            @Override
-            protected void deleteNode() {
-                // NOP
-            }
-        };
+        node =
+                new PersistentNode(
+                        client, CreateMode.PERSISTENT_WITH_TTL, false, path, 
initData, ttlMs, useParentCreation) {
+                    @Override
+                    protected void deleteNode() {
+                        // NOP
+                    }
+                };
         this.executorService = Objects.requireNonNull(executorService, 
"executorService cannot be null");
         childPath = ZKPaths.makePath(Objects.requireNonNull(path, "path cannot 
be null"), childNodeName);
     }
 
+    @VisibleForTesting
+    void touch() {
+        try {
+            try {
+                client.setData().forPath(childPath);
+            } catch (KeeperException.NoNodeException e) {
+                client.create()
+                        .orSetData()
+                        .withTtl(ttlMs)
+                        .withMode(CreateMode.PERSISTENT_WITH_TTL)
+                        .forPath(childPath);
+            }
+        } catch (KeeperException.NoNodeException ignore) {
+            // ignore
+        } catch (Exception e) {
+            if (!ThreadUtils.checkInterrupted(e)) {
+                log.debug("Could not touch child node", e);
+            }
+        }
+    }
+
     /**
      * You must call start() to initiate the persistent ttl node
      */
     public void start() {
         node.start();
-
-        Runnable touchTask = new Runnable() {
-            @Override
-            public void run() {
-                try {
-                    try {
-                        client.setData().forPath(childPath);
-                    } catch (KeeperException.NoNodeException e) {
-                        client.create()
-                                .orSetData()
-                                .withTtl(ttlMs)
-                                .withMode(CreateMode.PERSISTENT_WITH_TTL)
-                                .forPath(childPath);
-                    }
-                } catch (KeeperException.NoNodeException ignore) {
-                    // ignore
-                } catch (Exception e) {
-                    if (!ThreadUtils.checkInterrupted(e)) {
-                        log.debug("Could not touch child node", e);
-                    }
-                }
-            }
-        };
         Future<?> future = executorService.scheduleAtFixedRate(
-                touchTask, ttlMs / touchScheduleFactor, ttlMs / 
touchScheduleFactor, TimeUnit.MILLISECONDS);
+                this::touch, ttlMs / touchScheduleFactor, ttlMs / 
touchScheduleFactor, TimeUnit.MILLISECONDS);
         futureRef.set(future);
     }
 
diff --git 
a/curator-recipes/src/test/java/org/apache/curator/framework/recipes/nodes/TestPersistentTtlNode.java
 
b/curator-recipes/src/test/java/org/apache/curator/framework/recipes/nodes/TestPersistentTtlNode.java
index ddc9f3975..b98ada445 100644
--- 
a/curator-recipes/src/test/java/org/apache/curator/framework/recipes/nodes/TestPersistentTtlNode.java
+++ 
b/curator-recipes/src/test/java/org/apache/curator/framework/recipes/nodes/TestPersistentTtlNode.java
@@ -22,22 +22,28 @@ package org.apache.curator.framework.recipes.nodes;
 import static 
org.apache.curator.framework.recipes.cache.PathChildrenCache.StartMode.BUILD_INITIAL_CACHE;
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 import org.apache.curator.framework.CuratorFramework;
 import org.apache.curator.framework.CuratorFrameworkFactory;
 import org.apache.curator.framework.recipes.cache.PathChildrenCache;
 import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
 import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
+import org.apache.curator.framework.recipes.watch.PersistentWatcher;
 import org.apache.curator.retry.RetryOneTime;
 import org.apache.curator.test.Timing;
 import org.apache.curator.test.compatibility.CuratorTestBase;
 import org.apache.curator.test.compatibility.Timing2;
 import org.apache.curator.utils.ZKPaths;
+import org.apache.zookeeper.Watcher;
+import org.apache.zookeeper.Watcher.Event.EventType;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
@@ -187,4 +193,48 @@ public class TestPersistentTtlNode extends CuratorTestBase 
{
             assertNull(client.checkExists().forPath("/test"));
         }
     }
+
+    @Test
+    public void testTouchNodeNotCreated() throws Exception {
+        final String mainPath = "/parent/main";
+        final String touchPath = ZKPaths.makePath(mainPath, 
PersistentTtlNode.DEFAULT_CHILD_NODE_NAME);
+        final long testTtlMs = 500L;
+        final CountDownLatch mainCreatedLatch = new CountDownLatch(1);
+        final CountDownLatch mainDeletedLatch = new CountDownLatch(1);
+        final AtomicBoolean touchCreated = new AtomicBoolean();
+        try (CuratorFramework client =
+                CuratorFrameworkFactory.newClient(server.getConnectString(), 
new RetryOneTime(1))) {
+            client.start();
+            assertTrue(client.blockUntilConnected(1, TimeUnit.SECONDS));
+            try (PersistentWatcher watcher = new PersistentWatcher(client, 
mainPath, true)) {
+                final Watcher listener = event -> {
+                    final String path = event.getPath();
+                    if (mainPath.equals(path)) {
+                        final EventType type = event.getType();
+                        if (EventType.NodeCreated.equals(type)) {
+                            mainCreatedLatch.countDown();
+                        } else if (EventType.NodeDeleted.equals(type)) {
+                            mainDeletedLatch.countDown();
+                        }
+                    } else if (touchPath.equals(path)) {
+                        touchCreated.set(true);
+                    }
+                };
+                watcher.getListenable().addListener(listener);
+                watcher.start();
+                try (PersistentTtlNode node = new PersistentTtlNode(client, 
mainPath, testTtlMs, new byte[0]) {
+                    @Override
+                    void touch() {
+                        // NOP
+                    }
+                }) {
+                    node.start();
+                    assertTrue(mainCreatedLatch.await(1L, TimeUnit.SECONDS));
+                }
+                assertNull(client.checkExists().forPath(touchPath));
+                assertTrue(mainDeletedLatch.await(3L * testTtlMs, 
TimeUnit.MILLISECONDS));
+                assertFalse(touchCreated.get()); // Just to control that touch 
ZNode never created
+            }
+        }
+    }
 }

Reply via email to