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

anmolnar pushed a commit to branch branch-3.9
in repository https://gitbox.apache.org/repos/asf/zookeeper.git


The following commit(s) were added to refs/heads/branch-3.9 by this push:
     new 91cac7cc3 ZOOKEEPER-5091: WatchManager reports connections with no 
active watches after consuming the last standard watch
91cac7cc3 is described below

commit 91cac7cc31b0c0e34c44c4945ac72759dc7395d1
Author: casolaroSalvatore <[email protected]>
AuthorDate: Tue Oct 6 01:44:10 2026 +0200

    ZOOKEEPER-5091: WatchManager reports connections with no active watches 
after consuming the last standard watch
    
    Reviewers: kezhuw, anmolnar
    Author: casolaroSalvatore
    Closes #2457 from casolaroSalvatore/ZOOKEEPER-5091
---
 .../zookeeper/server/watch/WatchManager.java       |  3 +
 .../zookeeper/server/watch/WatchManagerTest.java   | 87 ++++++++++++++++++++++
 2 files changed, 90 insertions(+)

diff --git 
a/zookeeper-server/src/main/java/org/apache/zookeeper/server/watch/WatchManager.java
 
b/zookeeper-server/src/main/java/org/apache/zookeeper/server/watch/WatchManager.java
index d1c743104..f10b21aec 100644
--- 
a/zookeeper-server/src/main/java/org/apache/zookeeper/server/watch/WatchManager.java
+++ 
b/zookeeper-server/src/main/java/org/apache/zookeeper/server/watch/WatchManager.java
@@ -162,6 +162,9 @@ public WatcherOrBitSet triggerWatch(String path, EventType 
type, long zxid, List
                         if (newStats == WatchStats.NONE) {
                             iterator.remove();
                             paths.remove(localPath);
+                            if (paths.isEmpty()) {
+                                watch2Paths.remove(watcher);
+                            }
                         } else if (newStats != stats) {
                             paths.put(localPath, newStats);
                         }
diff --git 
a/zookeeper-server/src/test/java/org/apache/zookeeper/server/watch/WatchManagerTest.java
 
b/zookeeper-server/src/test/java/org/apache/zookeeper/server/watch/WatchManagerTest.java
index 51bbb94d8..4ff5c305d 100644
--- 
a/zookeeper-server/src/test/java/org/apache/zookeeper/server/watch/WatchManagerTest.java
+++ 
b/zookeeper-server/src/test/java/org/apache/zookeeper/server/watch/WatchManagerTest.java
@@ -20,6 +20,8 @@
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -372,6 +374,91 @@ public void testAddRemoveWatcher(String className) throws 
IOException {
         assertFalse(manager.removeWatcher("/node1", watcher1));
     }
 
+    /**
+     * Test triggering the last standard watch removes the watcher from the 
watches summary.
+     */
+    @Test
+    public void testTriggerLastStandardWatchRemovesWatcherFromSummary() {
+        WatchManager manager = new WatchManager();
+        DumbWatcher watcher = new DumbWatcher(0x40L);
+
+        // given: add a standard watch to "/node1"
+        assertTrue(manager.addWatch("/node1", watcher, WatcherMode.STANDARD));
+
+        // when: trigger the standard watch
+        WatcherOrBitSet triggered = manager.triggerWatch("/node1", 
EventType.NodeDataChanged, 1L, null, null);
+
+        // then: the standard watch should be triggered and consumed
+        assertNotNull(triggered);
+        assertTrue(triggered.contains(watcher));
+        checkMostRecentWatchedEvent(watcher, "/node1", 
EventType.NodeDataChanged, 1L);
+        assertFalse(manager.containsWatcher("/node1", watcher, 
WatcherMode.STANDARD));
+        assertEquals(0, manager.size());
+
+        // then: the watches summary should not retain the watcher
+        WatchesSummary summary = manager.getWatchesSummary();
+        assertEquals(0, summary.getNumConnections());
+        assertEquals(0, summary.getNumPaths());
+        assertEquals(0, summary.getTotalWatches());
+    }
+
+    /**
+     * Test triggering the last standard watch removes the watcher from the 
watches report.
+     */
+    @Test
+    public void testTriggerLastStandardWatchRemovesWatcherFromWatchesReport() {
+        WatchManager manager = new WatchManager();
+        DumbWatcher watcher = new DumbWatcher(0x40L);
+
+        // given: add a standard watch to "/node1"
+        assertTrue(manager.addWatch("/node1", watcher, WatcherMode.STANDARD));
+
+        // when: trigger the standard watch
+        WatcherOrBitSet triggered = manager.triggerWatch("/node1", 
EventType.NodeDataChanged, 1L, null, null);
+
+        // then: the standard watch should be triggered and consumed
+        assertNotNull(triggered);
+        assertTrue(triggered.contains(watcher));
+        assertFalse(manager.containsWatcher("/node1", watcher, 
WatcherMode.STANDARD));
+        assertEquals(0, manager.size());
+
+        // then: the watches report should not retain the watcher
+        WatchesReport report = manager.getWatches();
+        assertNull(report.getPaths(0x40L));
+    }
+
+    /**
+     * Test triggering a standard watch preserves other registrations
+     * belonging to the same watcher.
+     */
+    @Test
+    public void testTriggerStandardWatchPreservesOtherWatcherRegistration() {
+        WatchManager manager = new WatchManager();
+        DumbWatcher watcher = new DumbWatcher(0x40L);
+
+        // given: add a standard watch and a persistent watch on different 
paths
+        assertTrue(manager.addWatch("/node1", watcher, WatcherMode.STANDARD));
+        assertTrue(manager.addWatch("/node2", watcher, 
WatcherMode.PERSISTENT));
+
+        // when: trigger the standard watch
+        WatcherOrBitSet triggered = manager.triggerWatch("/node1", 
EventType.NodeDataChanged, 1L, null, null);
+
+        // then: the standard watch should be triggered and consumed
+        assertNotNull(triggered);
+        assertTrue(triggered.contains(watcher));
+        assertFalse(manager.containsWatcher("/node1", watcher, 
WatcherMode.STANDARD));
+
+        // then: the persistent registration on the other path should remain
+        assertTrue(manager.containsWatcher("/node2", watcher, 
WatcherMode.PERSISTENT));
+
+        WatchesSummary summary = manager.getWatchesSummary();
+        assertEquals(1, summary.getNumConnections());
+        assertEquals(1, summary.getNumPaths());
+        assertEquals(1, summary.getTotalWatches());
+
+        assertEquals(Set.of("/node2"), manager.getWatches().getPaths(0x40L));
+    }
+
     /**
      * Test containsWatcher on all pairs, and removeWatcher on mismatch pairs.
      */

Reply via email to