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

AlexStocks pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-go.git


The following commit(s) were added to refs/heads/develop by this push:
     new 5683612b1 fix: remove stale providers after initial subscribe load 
(#3479)
5683612b1 is described below

commit 5683612b1dfbbf755e5bb10bfcc6faf56d7d1d19
Author: AsperforMias <[email protected]>
AuthorDate: Wed Jul 8 19:27:28 2026 +0800

    fix: remove stale providers after initial subscribe load (#3479)
    
    * fix(registry/nacos): seed listener with initial instances
---
 registry/nacos/listener.go      |  27 ++++++-
 registry/nacos/registry.go      |  40 +++++++++--
 registry/nacos/registry_test.go | 155 ++++++++++++++++++++++++++++++++++++++--
 3 files changed, 210 insertions(+), 12 deletions(-)

diff --git a/registry/nacos/listener.go b/registry/nacos/listener.go
index d9b0bd01c..8a5971642 100644
--- a/registry/nacos/listener.go
+++ b/registry/nacos/listener.go
@@ -74,6 +74,29 @@ func NewNacosListenerWithServiceName(serviceName string, 
regURL *common.URL, nam
        }
 }
 
+func (nl *nacosListener) setInstanceSnapshot(instances []model.Instance) {
+       nl.cacheLock.Lock()
+       defer nl.cacheLock.Unlock()
+
+       nl.instanceMap = buildInstanceMap(instances)
+}
+
+func buildInstanceMap(instances []model.Instance) map[string]model.Instance {
+       instanceMap := make(map[string]model.Instance, len(instances))
+       for i := range instances {
+               if !instances[i].Enable {
+                       continue
+               }
+               host := instanceHost(instances[i])
+               instanceMap[host] = instances[i]
+       }
+       return instanceMap
+}
+
+func instanceHost(instance model.Instance) string {
+       return instance.Ip + ":" + strconv.Itoa(int(instance.Port))
+}
+
 func generateUrl(instance model.Instance) *common.URL {
        if instance.Metadata == nil {
                logger.Errorf("[Registry][Nacos] nacos instance metadata is 
empty, instance=%+v", instance)
@@ -125,7 +148,7 @@ func (nl *nacosListener) Callback(services 
[]model.Instance, err error) {
                        // instance is not available,so ignore it
                        continue
                }
-               host := services[i].Ip + ":" + 
strconv.Itoa(int(services[i].Port))
+               host := instanceHost(services[i])
                instance := services[i]
                newInstanceMap[host] = instance
                if old, ok := nl.instanceMap[host]; !ok && instance.Healthy {
@@ -184,7 +207,7 @@ func (nl *nacosListener) listenService(serviceName string) 
error {
        }
        err := nl.namingClient.Client().Subscribe(nl.subscribeParam)
        if err == nil {
-               
listenerCache.Store(nl.subscribeParam.ServiceName+nl.subscribeParam.GroupName, 
nl)
+               
listenerCache.Store(subscribeCacheKey(nl.subscribeParam.ServiceName, 
nl.subscribeParam.GroupName), nl)
        }
        return nil
 }
diff --git a/registry/nacos/registry.go b/registry/nacos/registry.go
index 745f1db53..7b47366ff 100644
--- a/registry/nacos/registry.go
+++ b/registry/nacos/registry.go
@@ -34,6 +34,7 @@ import (
        nacosClient "github.com/dubbogo/gost/database/kv/nacos"
        "github.com/dubbogo/gost/log/logger"
 
+       "github.com/nacos-group/nacos-sdk-go/v2/model"
        "github.com/nacos-group/nacos-sdk-go/v2/vo"
 
        perrors "github.com/pkg/errors"
@@ -61,11 +62,14 @@ func init() {
 
 type nacosRegistry struct {
        *common.URL
+       // namingClient is the pooled Nacos client shared by registry 
operations and listeners.
        namingClient *nacosClient.NacosNamingClient
        registryUrls []*common.URL
-       done         chan struct{}
-       availability availabilityCache
-       wg           sync.WaitGroup
+       // initialSubscribeInstances keeps the synchronous 
LoadSubscribeInstances result as the diff baseline for the first Nacos push.
+       initialSubscribeInstances sync.Map
+       done                      chan struct{}
+       availability              availabilityCache
+       wg                        sync.WaitGroup
 }
 
 type availabilityCache struct {
@@ -256,7 +260,7 @@ func (nr *nacosRegistry) subscribeAll(url *common.URL, 
notifyListener registry.N
                return
        }
        for _, name := range serviceNames {
-               if _, ok := listenerCache.Load(name + groupName); ok {
+               if _, ok := listenerCache.Load(subscribeCacheKey(name, 
groupName)); ok {
                        // has subscribed ,ignore
                        continue
                }
@@ -279,6 +283,13 @@ func (nr *nacosRegistry) subscribe(serviceName string, 
notifyListener registry.N
                return perrors.New("nacosRegistry is not available.")
        }
        listener := NewNacosListenerWithServiceName(serviceName, nr.URL, 
nr.namingClient)
+       groupName := nr.GetParam(constant.RegistryGroupKey, defaultGroup)
+       cacheKey := subscribeCacheKey(serviceName, groupName)
+       if value, ok := nr.initialSubscribeInstances.Load(cacheKey); ok {
+               if instances, ok := value.([]model.Instance); ok {
+                       listener.setInstanceSnapshot(instances)
+               }
+       }
        // will add to listenerCache when subscribe success
        err := listener.listenService(serviceName)
        metrics.Publish(metricsRegistry.NewSubscribeEvent(err == nil))
@@ -286,6 +297,7 @@ func (nr *nacosRegistry) subscribe(serviceName string, 
notifyListener registry.N
                logger.Warnf("[Registry][Nacos] subscribe service %s err=%v", 
serviceName, perrors.WithStack(err))
                return err
        }
+       nr.initialSubscribeInstances.Delete(cacheKey)
        // handleServiceEvents will block to wait notify event and exit when 
error occur
        nr.wg.Add(1)
        go func() {
@@ -343,7 +355,9 @@ func (nr *nacosRegistry) handleServiceEvents(listener 
registry.Listener, notifyL
 
 // UnSubscribe :
 func (nr *nacosRegistry) UnSubscribe(url *common.URL, _ 
registry.NotifyListener) error {
-       param := createSubscribeParam(getSubscribeName(url), 
nr.GetParam(constant.RegistryGroupKey, defaultGroup), nil)
+       serviceName := getSubscribeName(url)
+       groupName := nr.GetParam(constant.RegistryGroupKey, defaultGroup)
+       param := createSubscribeParam(serviceName, groupName, nil)
        if param == nil {
                return nil
        }
@@ -351,6 +365,7 @@ func (nr *nacosRegistry) UnSubscribe(url *common.URL, _ 
registry.NotifyListener)
        if err != nil {
                return perrors.New("UnSubscribe [" + param.ServiceName + "] to 
nacos failed")
        }
+       nr.initialSubscribeInstances.Delete(subscribeCacheKey(serviceName, 
groupName))
        return nil
 }
 
@@ -366,6 +381,11 @@ func (nr *nacosRegistry) LoadSubscribeInstances(url 
*common.URL, notify registry
                return perrors.New(fmt.Sprintf("could not query the instances 
for serviceName=%s,groupName=%s,error=%v",
                        serviceName, groupName, err))
        }
+       if len(instances) > 0 {
+               copied := make([]model.Instance, len(instances))
+               copy(copied, instances)
+               
nr.initialSubscribeInstances.Store(subscribeCacheKey(serviceName, groupName), 
copied)
+       }
 
        for i := range instances {
                if newUrl := generateUrl(instances[i]); newUrl != nil {
@@ -375,9 +395,13 @@ func (nr *nacosRegistry) LoadSubscribeInstances(url 
*common.URL, notify registry
        return nil
 }
 
+func subscribeCacheKey(serviceName string, groupName string) string {
+       return fmt.Sprintf("%d:%s:%s", len(serviceName), serviceName, groupName)
+}
+
 func createSubscribeParam(serviceName string, groupName string, cb callback) 
*vo.SubscribeParam {
        if cb == nil {
-               v, ok := listenerCache.Load(serviceName + groupName)
+               v, ok := listenerCache.Load(subscribeCacheKey(serviceName, 
groupName))
                if !ok {
                        return nil
                }
@@ -453,6 +477,10 @@ func (nr *nacosRegistry) Destroy() {
        }
 
        nr.registryUrls = nil
+       nr.initialSubscribeInstances.Range(func(key any, _ any) bool {
+               nr.initialSubscribeInstances.Delete(key)
+               return true
+       })
        nr.CloseAndNilClient()
 }
 
diff --git a/registry/nacos/registry_test.go b/registry/nacos/registry_test.go
index 3b157b4d2..d7ae17533 100644
--- a/registry/nacos/registry_test.go
+++ b/registry/nacos/registry_test.go
@@ -40,6 +40,7 @@ import (
        "dubbo.apache.org/dubbo-go/v3/common"
        "dubbo.apache.org/dubbo-go/v3/common/constant"
        "dubbo.apache.org/dubbo-go/v3/registry"
+       "dubbo.apache.org/dubbo-go/v3/remoting"
 )
 
 // MockINamingClient is a mock of INamingClient interface
@@ -426,7 +427,7 @@ func TestNacosRegistryDestroy(t *testing.T) {
                SubscribeCallback: nl.Callback,
        }
        nl.subscribeParam = subscribeParam
-       listenerCache.Store(serviceName+"testgroup", nl)
+       listenerCache.Store(subscribeCacheKey(serviceName, "testgroup"), nl)
 
        // Simulate unsubscribe and unregister instances
        mockNamingClient.EXPECT().Unsubscribe(subscribeParam).Return(nil)
@@ -451,7 +452,7 @@ func TestNacosRegistryDestroy(t *testing.T) {
                t.Errorf("namingClient was not set to nil")
        }
 
-       if _, ok := listenerCache.Load(serviceName + "testgroup"); ok {
+       if _, ok := listenerCache.Load(subscribeCacheKey(serviceName, 
"testgroup")); ok {
                t.Errorf("listenerCache was not cleared")
        }
 }
@@ -601,7 +602,7 @@ func TestNacosRegistryCloseListener(t *testing.T) {
                SubscribeCallback: nl.Callback,
        }
        nl.subscribeParam = subscribeParam
-       listenerCache.Store(serviceName+"testgroup", nl)
+       listenerCache.Store(subscribeCacheKey(serviceName, "testgroup"), nl)
 
        mockNamingClient.EXPECT().Unsubscribe(subscribeParam).Return(nil)
 
@@ -610,7 +611,7 @@ func TestNacosRegistryCloseListener(t *testing.T) {
        nr.CloseListener()
 
        // Verify whether to clear the entries in the listenerCache
-       if _, ok := listenerCache.Load(serviceName + "testgroup"); ok {
+       if _, ok := listenerCache.Load(subscribeCacheKey(serviceName, 
"testgroup")); ok {
                t.Errorf("listenerCache was not cleared")
        }
 
@@ -685,3 +686,149 @@ func 
TestNacosRegistryScheduledLookUpStopsWhenDoneClosed(t *testing.T) {
                t.Fatalf("scheduledLookUp should exit quickly after done is 
closed")
        }
 }
+
+func TestNacosRegistryInitialSubscribeSnapshotDiff(t *testing.T) {
+       clearListenerCacheForTest()
+
+       ctrl := gomock.NewController(t)
+       defer ctrl.Finish()
+
+       mockNamingClient := NewMockINamingClient(ctrl)
+       nc := &nacosClient.NacosNamingClient{}
+       nc.SetClient(mockNamingClient)
+
+       regURL, _ := 
common.NewURL("registry://127.0.0.1:8848?registry.group=testgroup")
+       nr := &nacosRegistry{
+               URL:          regURL,
+               namingClient: nc,
+               done:         make(chan struct{}),
+               registryUrls: []*common.URL{},
+       }
+
+       urlMap := url.Values{}
+       urlMap.Set(constant.RegistryRoleKey, strconv.Itoa(common.CONSUMER))
+       urlMap.Set(constant.InterfaceKey, "com.test.InitialSnapshotService")
+       testURL, _ := 
common.NewURL("consumer://127.0.0.1/com.test.InitialSnapshotService", 
common.WithParams(urlMap))
+
+       instanceA := newNacosTestInstance("10.0.0.1", 20001, 
"com.test.InitialSnapshotService")
+       instanceB := newNacosTestInstance("10.0.0.2", 20002, 
"com.test.InitialSnapshotService")
+
+       var subscribeParam *vo.SubscribeParam
+       
mockNamingClient.EXPECT().SelectAllInstances(gomock.Any()).Return([]model.Instance{instanceA,
 instanceB}, nil)
+       
mockNamingClient.EXPECT().GetAllServicesInfo(gomock.Any()).Return(model.ServiceList{},
 nil).AnyTimes()
+       
mockNamingClient.EXPECT().Subscribe(gomock.Any()).DoAndReturn(func(param 
*vo.SubscribeParam) error {
+               subscribeParam = param
+               return nil
+       })
+       mockNamingClient.EXPECT().Unsubscribe(gomock.Any()).AnyTimes()
+
+       notify := newRecordingNotifyListener()
+       if err := nr.LoadSubscribeInstances(testURL, notify); err != nil {
+               t.Fatalf("LoadSubscribeInstances() error = %v", err)
+       }
+       assertRecordedEvent(t, notify.nextEvent(t), remoting.EventTypeAdd, 
"10.0.0.1", "20001")
+       assertRecordedEvent(t, notify.nextEvent(t), remoting.EventTypeAdd, 
"10.0.0.2", "20002")
+
+       if err := nr.subscribe(getSubscribeName(testURL), notify); err != nil {
+               t.Fatalf("subscribe() error = %v", err)
+       }
+       if subscribeParam == nil || subscribeParam.SubscribeCallback == nil {
+               t.Fatalf("SubscribeCallback was not captured")
+       }
+
+       subscribeParam.SubscribeCallback([]model.Instance{instanceA}, nil)
+       assertRecordedEvent(t, notify.nextEvent(t), remoting.EventTypeDel, 
"10.0.0.2", "20002")
+       notify.assertNoEvent(t)
+
+       nr.CloseListener()
+       close(nr.done)
+       nr.wg.Wait()
+}
+
+func clearListenerCacheForTest() {
+       listenerCache.Range(func(key any, _ any) bool {
+               listenerCache.Delete(key)
+               return true
+       })
+}
+
+func newNacosTestInstance(ip string, port uint64, interfaceName string) 
model.Instance {
+       return model.Instance{
+               Ip:        ip,
+               Port:      port,
+               Enable:    true,
+               Healthy:   true,
+               Ephemeral: true,
+               Metadata: map[string]string{
+                       constant.InterfaceKey:           interfaceName,
+                       constant.NacosPathKey:           "/" + interfaceName,
+                       constant.NacosProtocolKey:       "tri",
+                       constant.NacosCategoryKey:       
common.DubboNodes[common.PROVIDER],
+                       constant.RegistryRoleKey:        
strconv.Itoa(common.PROVIDER),
+                       constant.RegistryGroupKey:       "testgroup",
+                       constant.SerializationKey:       "protobuf",
+                       constant.ReleaseKey:             "test",
+                       constant.ApplicationKey:         "test-provider",
+                       constant.MethodsKey:             "",
+                       constant.TimestampKey:           "1",
+                       constant.AnyhostKey:             "true",
+                       constant.TokenKey:               "",
+                       constant.WarmupKey:              "",
+                       constant.SideKey:                "provider",
+                       constant.NacosGroupKey:          "testgroup",
+                       constant.NacosNamespaceID:       "",
+                       constant.NacosNotLoadLocalCache: "true",
+               },
+       }
+}
+
+type recordingNotifyListener struct {
+       events chan *registry.ServiceEvent
+}
+
+func newRecordingNotifyListener() *recordingNotifyListener {
+       return &recordingNotifyListener{events: make(chan 
*registry.ServiceEvent, 8)}
+}
+
+func (r *recordingNotifyListener) Notify(event *registry.ServiceEvent) {
+       r.events <- event
+}
+
+func (r *recordingNotifyListener) NotifyAll(events []*registry.ServiceEvent, 
callback func()) {
+       for _, event := range events {
+               r.Notify(event)
+       }
+       if callback != nil {
+               callback()
+       }
+}
+
+func (r *recordingNotifyListener) nextEvent(t *testing.T) 
*registry.ServiceEvent {
+       t.Helper()
+       select {
+       case event := <-r.events:
+               return event
+       case <-time.After(time.Second):
+               t.Fatal("timed out waiting for notify event")
+               return nil
+       }
+}
+
+func (r *recordingNotifyListener) assertNoEvent(t *testing.T) {
+       t.Helper()
+       select {
+       case event := <-r.events:
+               t.Fatalf("unexpected notify event: action=%s service=%s", 
event.Action, event.Service)
+       case <-time.After(100 * time.Millisecond):
+       }
+}
+
+func assertRecordedEvent(t *testing.T, event *registry.ServiceEvent, action 
remoting.EventType, ip string, port string) {
+       t.Helper()
+       if event.Action != action {
+               t.Fatalf("event action = %s, want %s", event.Action, action)
+       }
+       if event.Service.Ip != ip || event.Service.Port != port {
+               t.Fatalf("event service = %s:%s, want %s:%s", event.Service.Ip, 
event.Service.Port, ip, port)
+       }
+}

Reply via email to