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

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


The following commit(s) were added to refs/heads/master by this push:
     new 6db6b391ca fix(zookeeper): read the deleted node from oldData in 
instance watchers (#7326)
6db6b391ca is described below

commit 6db6b391ca684ecca8c80a2468947b55f81d0a9c
Author: Sean-Walker0 <[email protected]>
AuthorDate: Sun Sep 27 12:44:50 2026 +0800

    fix(zookeeper): read the deleted node from oldData in instance watchers 
(#7326)
    
    CuratorCacheListener delivers NODE_DELETED with a null new ChildData
    and the deleted node in oldData. The watcher's first guard bailed out
    whenever data was null, before the switch, so the NODE_DELETED branch
    was unreachable: instance-removal events were never surfaced and admin
    kept stale upstreams for Zookeeper-registered services until a full
    resync. Etcd's sibling repository handles its DELETE from the event key
    the same way this change now does for Zookeeper.
    
    Use oldData as the changed node for NODE_DELETED (and data otherwise),
    keeping the null guards for safety. The new test fires a
    NODE_DELETED event through a captured listener and fails on current
    master (zero interactions) and passes with this change.
    
    Co-authored-by: Sean-Walker0 
<[email protected]>
    Co-authored-by: aias00 <[email protected]>
---
 .../ZookeeperInstanceRegisterRepository.java       | 12 ++++++----
 .../ZookeeperInstanceRegisterRepositoryTest.java   | 28 ++++++++++++++++++++++
 2 files changed, 36 insertions(+), 4 deletions(-)

diff --git 
a/shenyu-registry/shenyu-registry-zookeeper/src/main/java/org/apache/shenyu/registry/zookeeper/ZookeeperInstanceRegisterRepository.java
 
b/shenyu-registry/shenyu-registry-zookeeper/src/main/java/org/apache/shenyu/registry/zookeeper/ZookeeperInstanceRegisterRepository.java
index 4404c440b8..86edd43333 100644
--- 
a/shenyu-registry/shenyu-registry-zookeeper/src/main/java/org/apache/shenyu/registry/zookeeper/ZookeeperInstanceRegisterRepository.java
+++ 
b/shenyu-registry/shenyu-registry-zookeeper/src/main/java/org/apache/shenyu/registry/zookeeper/ZookeeperInstanceRegisterRepository.java
@@ -22,7 +22,9 @@ import com.google.common.collect.Multimap;
 import com.google.gson.JsonObject;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.curator.framework.api.CuratorWatcher;
+import org.apache.curator.framework.recipes.cache.ChildData;
 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.shenyu.common.constant.Constants;
 import org.apache.shenyu.common.exception.ShenyuException;
@@ -186,13 +188,15 @@ public class ZookeeperInstanceRegisterRepository 
implements ShenyuInstanceRegist
     public void watchInstances(final String key, final ChangedEventListener 
listener) {
         try {
             CuratorCache treeCache = client.addCache(key, (type, oldData, 
data) -> {
-                if (!Objects.nonNull(data) || 
!Objects.nonNull(data.getData())) {
+                // Curator delivers NODE_DELETED with a null new ChildData and 
the deleted node in oldData
+                ChildData changedNode = CuratorCacheListener.Type.NODE_DELETED 
== type ? oldData : data;
+                if (!Objects.nonNull(changedNode) || 
!Objects.nonNull(changedNode.getData())) {
                     return;
                 }
-                String currentPath = data.getPath();
-                String currentData = new String(data.getData(), 
StandardCharsets.UTF_8);
+                String currentPath = changedNode.getPath();
+                String currentData = new String(changedNode.getData(), 
StandardCharsets.UTF_8);
                 LOGGER.info("zookeeper registry watch find resultData ={}", 
currentData);
-                Stat stat = data.getStat();
+                Stat stat = changedNode.getStat();
                 boolean isEphemeral = Objects.nonNull(stat) && 
stat.getEphemeralOwner() > 0;
                 if (!isEphemeral) {
                     LOGGER.info("zookeeper registry watch Ignore non-ephemeral 
node changes path {}", currentPath);
diff --git 
a/shenyu-registry/shenyu-registry-zookeeper/src/test/java/org/apache/shenyu/registry/zookeeper/ZookeeperInstanceRegisterRepositoryTest.java
 
b/shenyu-registry/shenyu-registry-zookeeper/src/test/java/org/apache/shenyu/registry/zookeeper/ZookeeperInstanceRegisterRepositoryTest.java
index ad6838983b..9fda0407f5 100644
--- 
a/shenyu-registry/shenyu-registry-zookeeper/src/test/java/org/apache/shenyu/registry/zookeeper/ZookeeperInstanceRegisterRepositoryTest.java
+++ 
b/shenyu-registry/shenyu-registry-zookeeper/src/test/java/org/apache/shenyu/registry/zookeeper/ZookeeperInstanceRegisterRepositoryTest.java
@@ -19,10 +19,13 @@ package org.apache.shenyu.registry.zookeeper;
 
 import org.apache.curator.framework.CuratorFramework;
 import org.apache.curator.framework.api.CuratorWatcher;
+import org.apache.curator.framework.recipes.cache.ChildData;
+import org.apache.curator.framework.recipes.cache.CuratorCacheListener;
 import org.apache.curator.framework.listen.Listenable;
 import org.apache.curator.framework.state.ConnectionState;
 import org.apache.curator.framework.state.ConnectionStateListener;
 import org.apache.shenyu.common.utils.GsonUtils;
+import org.apache.shenyu.registry.api.event.ChangedEventListener;
 import org.apache.shenyu.infra.zookeeper.client.ZookeeperClient;
 import org.apache.shenyu.registry.api.config.RegisterConfig;
 import org.apache.shenyu.registry.api.entity.InstanceEntity;
@@ -105,4 +108,29 @@ public final class ZookeeperInstanceRegisterRepositoryTest 
{
         }
     }
 
+    @Test
+    public void testWatchInstancesEmitsDeletedEventForEphemeralNode() {
+        final Listenable listenable = mock(Listenable.class);
+        try (MockedConstruction<ZookeeperClient> construction = 
mockConstruction(ZookeeperClient.class, (mock, context) -> {
+            final CuratorFramework curatorFramework = 
mock(CuratorFramework.class);
+            when(mock.getClient()).thenReturn(curatorFramework);
+            
when(curatorFramework.getConnectionStateListenable()).thenReturn(listenable);
+        })) {
+            final ZookeeperInstanceRegisterRepository repository = new 
ZookeeperInstanceRegisterRepository();
+            RegisterConfig config = new RegisterConfig();
+            repository.init(config);
+            ZookeeperClient client = construction.constructed().get(0);
+            org.mockito.ArgumentCaptor<CuratorCacheListener> captor = 
org.mockito.ArgumentCaptor.forClass(CuratorCacheListener.class);
+            ChangedEventListener changedEventListener = 
mock(ChangedEventListener.class);
+            repository.watchInstances("/shenyu/register/instance", 
changedEventListener);
+            
org.mockito.Mockito.verify(client).addCache(org.mockito.ArgumentMatchers.eq("/shenyu/register/instance"),
 captor.capture());
+            org.apache.zookeeper.data.Stat stat = new 
org.apache.zookeeper.data.Stat();
+            stat.setEphemeralOwner(1L);
+            ChildData deletedNode = new 
ChildData("/shenyu/register/instance/app-host-9195", stat, 
"instance-data".getBytes(java.nio.charset.StandardCharsets.UTF_8));
+            // Curator delivers NODE_DELETED with a null new ChildData and the 
node in oldData
+            captor.getValue().event(CuratorCacheListener.Type.NODE_DELETED, 
deletedNode, null);
+            
org.mockito.Mockito.verify(changedEventListener).onEvent("/shenyu/register/instance/app-host-9195",
 "instance-data", ChangedEventListener.Event.DELETED);
+        }
+    }
+
 }

Reply via email to