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 + } + } + } }