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 0d09f0250 GH-1314: Fix PersistentWatcher with CuratorFramework 
implementations that are not CuratorFrameworkBase (#1315)
0d09f0250 is described below

commit 0d09f025051f3a4def75bdeb472b0e7a20a0b12b
Author: Andrey D. Shindarev <[email protected]>
AuthorDate: Mon Oct 26 15:01:37 2026 +0300

    GH-1314: Fix PersistentWatcher with CuratorFramework implementations that 
are not CuratorFrameworkBase (#1315)
    
    Use the underlying client only for CuratorFrameworkBase instances 
(namespaced
    facades, #1259) and fall back to the public getCuratorListenable() 
otherwise,
    so that PersistentWatcher and CuratorCache work with any CuratorFramework
    implementation again, e.g. a decorator of the public interface.
    
    Fixes #1314.
---
 .../framework/recipes/watch/PersistentWatcher.java | 13 +++-
 .../recipes/watch/TestPersistentWatcher.java       | 79 ++++++++++++++++++++++
 2 files changed, 89 insertions(+), 3 deletions(-)

diff --git 
a/curator-recipes/src/main/java/org/apache/curator/framework/recipes/watch/PersistentWatcher.java
 
b/curator-recipes/src/main/java/org/apache/curator/framework/recipes/watch/PersistentWatcher.java
index 8cc1e9d63..b994fe139 100644
--- 
a/curator-recipes/src/main/java/org/apache/curator/framework/recipes/watch/PersistentWatcher.java
+++ 
b/curator-recipes/src/main/java/org/apache/curator/framework/recipes/watch/PersistentWatcher.java
@@ -87,11 +87,18 @@ public class PersistentWatcher implements Closeable {
     public void start() {
         Preconditions.checkState(state.compareAndSet(State.LATENT, 
State.STARTED), "Already started");
         
client.getConnectionStateListenable().addListener(connectionStateListener);
-        // This could be a namespaced facade which does not support 
getCuratorListenable.
-        ((CuratorFrameworkBase) 
client).client().getCuratorListenable().addListener(clientCloseListener);
+        curatorListenable().addListener(clientCloseListener);
         reset();
     }
 
+    private Listenable<CuratorListener> curatorListenable() {
+        // A namespaced facade does not support getCuratorListenable(), so use 
the underlying client (#1259).
+        // Other CuratorFramework implementations are handled through the 
public API.
+        return client instanceof CuratorFrameworkBase
+                ? ((CuratorFrameworkBase) 
client).client().getCuratorListenable()
+                : client.getCuratorListenable();
+    }
+
     /**
      * Remove the watcher
      */
@@ -99,7 +106,7 @@ public class PersistentWatcher implements Closeable {
     public void close() {
         if (state.compareAndSet(State.STARTED, State.CLOSED)) {
             listeners.clear();
-            ((CuratorFrameworkBase) 
client).client().getCuratorListenable().removeListener(clientCloseListener);
+            curatorListenable().removeListener(clientCloseListener);
             
client.getConnectionStateListenable().removeListener(connectionStateListener);
             try {
                 
client.watchers().remove(watcher).guaranteed().inBackground().forPath(basePath);
diff --git 
a/curator-recipes/src/test/java/org/apache/curator/framework/recipes/watch/TestPersistentWatcher.java
 
b/curator-recipes/src/test/java/org/apache/curator/framework/recipes/watch/TestPersistentWatcher.java
index 48a2c6ee9..485b1aec3 100644
--- 
a/curator-recipes/src/test/java/org/apache/curator/framework/recipes/watch/TestPersistentWatcher.java
+++ 
b/curator-recipes/src/test/java/org/apache/curator/framework/recipes/watch/TestPersistentWatcher.java
@@ -22,6 +22,8 @@ package org.apache.curator.framework.recipes.watch;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Proxy;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.LinkedBlockingQueue;
@@ -155,6 +157,83 @@ public class TestPersistentWatcher extends CuratorTestBase 
{
         }
     }
 
+    // GH-1314 PersistentWatcher with a CuratorFramework that is not a 
CuratorFrameworkBase
+    @Test
+    public void testDelegatingClientWatching() throws Exception {
+        BlockingQueue<WatchedEvent> events = new LinkedBlockingQueue<>();
+
+        try (CuratorFramework client = CuratorFrameworkFactory.newClient(
+                server.getConnectString(), timing.session(), 
timing.connection(), new RetryOneTime(1))) {
+            client.start();
+            // given: connected curator client
+            client.blockUntilConnected();
+
+            // given: started persistent watcher with a delegating client
+            PersistentWatcher persistentWatcher = new 
PersistentWatcher(delegatingClient(client), "/top/main", true);
+            persistentWatcher.getListenable().addListener(events::add);
+            persistentWatcher.start();
+
+            // when: create paths
+            client.create().creatingParentsIfNeeded().forPath("/top/main/a");
+
+            // then: receive node watch events
+            WatchedEvent event1 = events.poll(5, TimeUnit.SECONDS);
+            assertNotNull(event1);
+            assertEquals(Watcher.Event.EventType.NodeCreated, 
event1.getType());
+            assertEquals("/top/main", event1.getPath());
+
+            WatchedEvent event2 = events.poll(5, TimeUnit.SECONDS);
+            assertNotNull(event2);
+            assertEquals(Watcher.Event.EventType.NodeCreated, 
event2.getType());
+            assertEquals("/top/main/a", event2.getPath());
+        }
+
+        // when: curator client closed
+        // then: listener get Closed notification
+        WatchedEvent event = events.poll(5, TimeUnit.SECONDS);
+        assertNotNull(event);
+        assertEquals(Watcher.Event.EventType.None, event.getType());
+        assertEquals(Watcher.Event.KeeperState.Closed, event.getState());
+    }
+
+    // GH-1314 Curator listeners set up through a delegating client are 
de-registered on close
+    @Test
+    public void testDelegatingClientCuratorListenersNotLeaking() {
+        try (CuratorFramework client = CuratorFrameworkFactory.newClient(
+                server.getConnectString(), timing.session(), 
timing.connection(), new RetryOneTime(1))) {
+            client.start();
+            CuratorFramework delegatingClient = delegatingClient(client);
+
+            for (int i = 0; i < 100; i++) {
+                try (PersistentWatcher persistentWatcher = new 
PersistentWatcher(delegatingClient, "/top/main", true)) {
+                    persistentWatcher.start();
+                }
+            }
+
+            assertEquals(
+                    0,
+                    ((StandardListenerManager<?>) 
client.getCuratorListenable()).size(),
+                    "Curator listeners set up by the now closed 
PersistentWatchers should have been de-registered");
+        }
+    }
+
+    /**
+     * A {@link CuratorFramework} implemented outside of Curator: it forwards 
every call to the given client
+     * and is not a {@link 
org.apache.curator.framework.imps.CuratorFrameworkBase}.
+     */
+    private static CuratorFramework delegatingClient(CuratorFramework client) {
+        return (CuratorFramework) Proxy.newProxyInstance(
+                CuratorFramework.class.getClassLoader(),
+                new Class<?>[] {CuratorFramework.class},
+                (proxy, method, args) -> {
+                    try {
+                        return method.invoke(client, args);
+                    } catch (InvocationTargetException e) {
+                        throw e.getCause();
+                    }
+                });
+    }
+
     private void internalTest(boolean recursive) throws Exception {
         try (CuratorFramework client = CuratorFrameworkFactory.newClient(
                 server.getConnectString(), timing.session(), 
timing.connection(), new RetryOneTime(1))) {

Reply via email to