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

Alanxtl 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 c7459997f feat(metadata): add concurrency safety for application-level 
metadata state (#3353) (#3367)
c7459997f is described below

commit c7459997f41fd6c606156e270f7eae6462b9d62e
Author: jieguo <[email protected]>
AuthorDate: Thu Jun 25 16:53:15 2026 +0800

    feat(metadata): add concurrency safety for application-level metadata state 
(#3353) (#3367)
    
    * feat(metadata): add internal RWMutex for MetadataInfo and fix json 
serialization (#3353)
    
    Add sync.RWMutex to MetadataInfo struct with json:"-" / hessian:"-"
    tags to skip serialization. All mutating methods (AddService, RemoveService,
    AddSubscribeURL, RemoveSubscribeURL) acquire the write lock, and read
    methods (GetExportedServiceURLs, GetSubscribedURLs, GetServices) acquire
    the read lock. The new GetServices method returns a snapshot copy.
    
    Signed-off-by: jieguo-coder <[email protected]>
    
    * feat(metadata): add lock protection for global registry and instances 
(#3353)
    
    Add sync.RWMutex to protect registryMetadataInfo in metadata.go and
    instances in report_instance.go. Extract getMetadataReportUnsafe helper
    to avoid reentrant RLock deadlock in GetMetadataReportByRegistry fallback.
    Fix nacos report_test to use pointer to MetadataInfo for json.Marshal.
    
    Signed-off-by: jieguo-coder <[email protected]>
    
    * feat(metadata): fix concurrency safety in listener callbacks and external 
calls (#3353)
    
    Add mutex locking to AddListenerAndNotify and RemoveListener to protect
    shared fields listeners and serviceUrls. Replace direct access to
    MetadataInfo.Services with safe GetServices method in OnEvent and
    convertV2 to prevent unprotected map reads.
    
    Signed-off-by: jieguo-coder <[email protected]>
    
    * style: format import blocks using dubbo-go imports-formatter
    
    Signed-off-by: jieguo-coder <[email protected]>
    
    * style: format metadata.go and report_instance.go to pass CI
    
    Signed-off-by: jieguo-coder <[email protected]>
    
    * fix(metadata): resolve data races pointed out in code review
    
    Signed-off-by: jieguo-coder <[email protected]>
    
    * test: fix failing tests and improve unit test coverage
    
    Signed-off-by: jieguo-coder <[email protected]>
    
    * test: fix testifylint error by avoiding require in goroutines
    
    Signed-off-by: jieguo-coder <[email protected]>
    
    * fix(metadata): deep copy ServiceInfo to prevent write-on-read data race 
and reduce lock granularity in report creation
    
    Signed-off-by: jieguo-coder <[email protected]>
    
    * chore: trigger CI re-run for codecov gpg issue
    
    * refactor(metadata): implement no-lock helper to fix data race and avoid 
deadlocks in ReplaceExportedServices
    
    Signed-off-by: jieguo-coder <[email protected]>
    
    * test: fix compilation error in instance changed listener tests by adding 
missing registryId argument
    
    Signed-off-by: jieguo-coder <[email protected]>
    
    * fix(metadata): add RLock around global map lookup in RemoveService and 
RemoveSubscribeURL to prevent concurrent map read/write
    
    Signed-off-by: jieguo-coder <[email protected]>
    
    ---------
    
    Signed-off-by: jieguo-coder <[email protected]>
---
 metadata/info/metadata_info.go                     | 64 +++++++++++++++++++++-
 metadata/info/metadata_info_test.go                | 21 +++++++
 metadata/metadata.go                               | 27 ++++++++-
 metadata/metadata_service.go                       |  8 ++-
 metadata/metadata_service_test.go                  | 33 +++++++++++
 metadata/metadata_test.go                          | 34 ++++++++++++
 metadata/report/nacos/report_test.go               |  4 +-
 metadata/report_instance.go                        | 16 +++++-
 .../service_instances_changed_listener_impl.go     |  7 ++-
 ...service_instances_changed_listener_impl_test.go | 12 ++++
 10 files changed, 216 insertions(+), 10 deletions(-)

diff --git a/metadata/info/metadata_info.go b/metadata/info/metadata_info.go
index 171438999..8843fb029 100644
--- a/metadata/info/metadata_info.go
+++ b/metadata/info/metadata_info.go
@@ -24,6 +24,7 @@ import (
        "sort"
        "strconv"
        "strings"
+       "sync"
 )
 
 import (
@@ -65,6 +66,7 @@ type MetadataInfo struct {
        Services              map[string]*ServiceInfo  
`json:"services,omitempty" hessian:"services"`
        exportedServiceURLs   map[string][]*common.URL `hessian:"-"` // server 
exported service urls
        subscribedServiceURLs map[string][]*common.URL `hessian:"-"` // client 
subscribed service urls
+       mu                    sync.RWMutex             `json:"-" hessian:"-"`
 }
 
 func NewAppMetadataInfo(app string) *MetadataInfo {
@@ -97,6 +99,15 @@ func (info *MetadataInfo) JavaClassName() string {
 
 // AddService add provider service info to MetadataInfo
 func (info *MetadataInfo) AddService(url *common.URL) {
+       info.mu.Lock()
+       defer info.mu.Unlock()
+
+       info.addServiceWithoutLock(url)
+}
+
+// addServiceWithoutLock adds a service URL without acquiring the lock.
+// The caller must hold info.mu.Lock() before calling this method.
+func (info *MetadataInfo) addServiceWithoutLock(url *common.URL) {
        service := NewServiceInfoWithURL(url)
        info.Services[service.GetMatchKey()] = service
        addUrl(info.exportedServiceURLs, url)
@@ -136,6 +147,9 @@ func deleteItem(slice []*common.URL, index int) 
[]*common.URL {
 }
 
 func (info *MetadataInfo) RemoveService(url *common.URL) {
+       info.mu.Lock()
+       defer info.mu.Unlock()
+
        service := NewServiceInfoWithURL(url)
        removeUrl(info.exportedServiceURLs, url)
        if replacement := info.findExportedServiceURL(service.GetMatchKey()); 
replacement != nil {
@@ -147,15 +161,24 @@ func (info *MetadataInfo) RemoveService(url *common.URL) {
 
 // AddSubscribeURL client subscribe a service url
 func (info *MetadataInfo) AddSubscribeURL(url *common.URL) {
+       info.mu.Lock()
+       defer info.mu.Unlock()
+
        addUrl(info.subscribedServiceURLs, url)
 }
 
 // RemoveSubscribeURL client unsubscribe a service url
 func (info *MetadataInfo) RemoveSubscribeURL(url *common.URL) {
+       info.mu.Lock()
+       defer info.mu.Unlock()
+
        removeUrl(info.subscribedServiceURLs, url)
 }
 
 func (info *MetadataInfo) GetExportedServiceURLs() []*common.URL {
+       info.mu.RLock()
+       defer info.mu.RUnlock()
+
        res := make([]*common.URL, 0)
        for _, urls := range info.exportedServiceURLs {
                res = append(res, urls...)
@@ -164,6 +187,9 @@ func (info *MetadataInfo) GetExportedServiceURLs() 
[]*common.URL {
 }
 
 func (info *MetadataInfo) GetSubscribedURLs() []*common.URL {
+       info.mu.RLock()
+       defer info.mu.RUnlock()
+
        res := make([]*common.URL, 0)
        for _, urls := range info.subscribedServiceURLs {
                res = append(res, urls...)
@@ -171,11 +197,27 @@ func (info *MetadataInfo) GetSubscribedURLs() 
[]*common.URL {
        return res
 }
 
+// GetServices returns a deep copy of the Services map for safe iteration by 
external callers.
+// Each ServiceInfo is fully copied with lazy fields eagerly populated to 
prevent write-on-read races.
+func (info *MetadataInfo) GetServices() map[string]*ServiceInfo {
+       info.mu.Lock()
+       defer info.mu.Unlock()
+
+       cp := make(map[string]*ServiceInfo, len(info.Services))
+       for k, v := range info.Services {
+               cp[k] = v.DeepCopy()
+       }
+       return cp
+}
+
 func (info *MetadataInfo) ReplaceExportedServices(urls []*common.URL) {
+       info.mu.Lock()
+       defer info.mu.Unlock()
+
        info.Services = make(map[string]*ServiceInfo)
        info.exportedServiceURLs = make(map[string][]*common.URL)
        for _, serviceURL := range urls {
-               info.AddService(serviceURL)
+               info.addServiceWithoutLock(serviceURL)
        }
 }
 
@@ -289,6 +331,26 @@ func (si *ServiceInfo) GetServiceKey() string {
        return si.ServiceKey
 }
 
+// DeepCopy returns a fully independent copy of ServiceInfo with lazy fields 
eagerly populated.
+func (si *ServiceInfo) DeepCopy() *ServiceInfo {
+       params := make(map[string]string, len(si.Params))
+       for k, v := range si.Params {
+               params[k] = v
+       }
+       return &ServiceInfo{
+               Name:       si.Name,
+               Group:      si.Group,
+               Version:    si.Version,
+               Protocol:   si.Protocol,
+               Port:       si.Port,
+               Path:       si.Path,
+               Params:     params,
+               ServiceKey: si.GetServiceKey(),
+               MatchKey:   si.GetMatchKey(),
+               URL:        si.URL,
+       }
+}
+
 // toDescString returns a deterministic string representation of ServiceInfo
 // for revision calculation. Aligned with Java dubbo 
ServiceInfo.toDescString().
 //
diff --git a/metadata/info/metadata_info_test.go 
b/metadata/info/metadata_info_test.go
index 242f28a39..59c99812c 100644
--- a/metadata/info/metadata_info_test.go
+++ b/metadata/info/metadata_info_test.go
@@ -205,6 +205,27 @@ func TestServiceInfoGetMatchKey(t *testing.T) {
        assert.NotEmpty(t, si.GetMatchKey())
 }
 
+func TestMetadataInfoGetServices(t *testing.T) {
+       metadataInfo := &MetadataInfo{
+               Services:              make(map[string]*ServiceInfo),
+               exportedServiceURLs:   make(map[string][]*common.URL),
+               subscribedServiceURLs: make(map[string][]*common.URL),
+       }
+       url, _ := 
common.NewURL("dubbo://127.0.0.1:20000?application=foo&category=providers&check=false&dubbo=dubbo-go+v1.5.0&interface=com.foo.Bar&methods=GetPetByID%2CGetPetTypes&organization=Apache&owner=foo&revision=1.0.0&side=provider&version=1.0.0")
+       metadataInfo.AddService(url)
+
+       services := metadataInfo.GetServices()
+       require.Len(t, services, 1)
+       assert.NotEmpty(t, services)
+
+       // GetServices returns a copy: modifying the original does not affect 
the snapshot
+       metadataInfo.RemoveService(url)
+       assert.Len(t, services, 1)
+
+       // A fresh call reflects the removal
+       assert.Empty(t, metadataInfo.GetServices())
+}
+
 func TestServiceInfoJavaClassName(t *testing.T) {
        assert.Equalf(t, "org.apache.dubbo.metadata.MetadataInfo", 
NewAppMetadataInfo("dubbo").JavaClassName(), "JavaClassName()")
 }
diff --git a/metadata/metadata.go b/metadata/metadata.go
index 15367cf1c..6117864aa 100644
--- a/metadata/metadata.go
+++ b/metadata/metadata.go
@@ -18,6 +18,10 @@
 // Package metadata collects and exposes information of all services for 
service discovery purpose.
 package metadata
 
+import (
+       "sync"
+)
+
 import (
        "dubbo.apache.org/dubbo-go/v3/common"
        "dubbo.apache.org/dubbo-go/v3/common/constant"
@@ -25,7 +29,8 @@ import (
 )
 
 var (
-       registryMetadataInfo                 = 
make(map[string]*info.MetadataInfo)
+       registryMetadataInfo = make(map[string]*info.MetadataInfo)
+       registryMetadataLock sync.RWMutex
        metadataService      MetadataService = 
&DefaultMetadataService{metadataMap: registryMetadataInfo}
 )
 
@@ -34,31 +39,44 @@ func GetMetadataService() MetadataService {
 }
 
 func GetMetadataInfo(registryId string) *info.MetadataInfo {
+       registryMetadataLock.RLock()
+       defer registryMetadataLock.RUnlock()
        return registryMetadataInfo[registryId]
 }
 
 func AddService(registryId string, url *common.URL) {
+       registryMetadataLock.Lock()
        if _, exist := registryMetadataInfo[registryId]; !exist {
                registryMetadataInfo[registryId] = info.NewMetadataInfo(
                        url.GetParam(constant.ApplicationKey, ""),
                        url.GetParam(constant.ApplicationTagKey, ""),
                )
        }
-       registryMetadataInfo[registryId].AddService(url)
+       metaInfo := registryMetadataInfo[registryId]
+       registryMetadataLock.Unlock()
+
+       metaInfo.AddService(url)
 }
 
 func AddSubscribeURL(registryId string, url *common.URL) {
+       registryMetadataLock.Lock()
        if _, exist := registryMetadataInfo[registryId]; !exist {
                registryMetadataInfo[registryId] = info.NewMetadataInfo(
                        url.GetParam(constant.ApplicationKey, ""),
                        url.GetParam(constant.ApplicationTagKey, ""),
                )
        }
-       registryMetadataInfo[registryId].AddSubscribeURL(url)
+       metaInfo := registryMetadataInfo[registryId]
+       registryMetadataLock.Unlock()
+
+       metaInfo.AddSubscribeURL(url)
 }
 
 func RemoveService(registryId string, url *common.URL) {
+       registryMetadataLock.RLock()
        metadataInfo, exist := registryMetadataInfo[registryId]
+       registryMetadataLock.RUnlock()
+
        if !exist {
                return
        }
@@ -66,7 +84,10 @@ func RemoveService(registryId string, url *common.URL) {
 }
 
 func RemoveSubscribeURL(registryId string, url *common.URL) {
+       registryMetadataLock.RLock()
        metadataInfo, exist := registryMetadataInfo[registryId]
+       registryMetadataLock.RUnlock()
+
        if !exist {
                return
        }
diff --git a/metadata/metadata_service.go b/metadata/metadata_service.go
index 8cf7da13b..353044c04 100644
--- a/metadata/metadata_service.go
+++ b/metadata/metadata_service.go
@@ -95,6 +95,8 @@ func (mts *DefaultMetadataService) GetMetadataInfo(revision 
string) (*info.Metad
        if revision == "" {
                return nil, nil
        }
+       registryMetadataLock.RLock()
+       defer registryMetadataLock.RUnlock()
        for _, metadataInfo := range mts.metadataMap {
                if metadataInfo.Revision == revision {
                        return metadataInfo, nil
@@ -106,6 +108,8 @@ func (mts *DefaultMetadataService) GetMetadataInfo(revision 
string) (*info.Metad
 
 // GetExportedServiceURLs get exported service urls
 func (mts *DefaultMetadataService) GetExportedServiceURLs() ([]*common.URL, 
error) {
+       registryMetadataLock.RLock()
+       defer registryMetadataLock.RUnlock()
        urls := make([]*common.URL, 0)
        for _, metadataInfo := range mts.metadataMap {
                urls = append(urls, metadataInfo.GetExportedServiceURLs()...)
@@ -124,6 +128,8 @@ func (mts *DefaultMetadataService) GetMetadataServiceURL() 
(*common.URL, error)
 }
 
 func (mts *DefaultMetadataService) GetSubscribedURLs() ([]*common.URL, error) {
+       registryMetadataLock.RLock()
+       defer registryMetadataLock.RUnlock()
        urls := make([]*common.URL, 0)
        for _, metadataInfo := range mts.metadataMap {
                urls = append(urls, metadataInfo.GetSubscribedURLs()...)
@@ -257,7 +263,7 @@ func (mtsV2 *MetadataServiceV2) GetMetadataInfo(ctx 
context.Context, req *triple
        return &tripleapi.MetadataInfoV2{
                App:      metadataInfo.App,
                Version:  metadataInfo.Revision,
-               Services: convertV2(metadataInfo.Services),
+               Services: convertV2(metadataInfo.GetServices()),
                Tag:      metadataInfo.Tag,
        }, err
 }
diff --git a/metadata/metadata_service_test.go 
b/metadata/metadata_service_test.go
index 67a0bb557..c0cf0d8a7 100644
--- a/metadata/metadata_service_test.go
+++ b/metadata/metadata_service_test.go
@@ -20,6 +20,7 @@ package metadata
 import (
        "context"
        "strconv"
+       "sync"
        "testing"
 )
 
@@ -391,3 +392,35 @@ func Test_serviceExporterExport(t *testing.T) {
                require.NoError(t, err)
        })
 }
+
+func TestDefaultMetadataServiceConcurrentReadAccess(t *testing.T) {
+       mts := &DefaultMetadataService{
+               metadataMap: newMetadataMap(),
+       }
+
+       var wg sync.WaitGroup
+       for i := 0; i < 10; i++ {
+               wg.Add(1)
+               go func() {
+                       defer wg.Done()
+                       urls, err := mts.GetExportedServiceURLs()
+                       assert.NoError(t, err)
+                       assert.NotEmpty(t, urls)
+               }()
+               wg.Add(1)
+               go func() {
+                       defer wg.Done()
+                       urls, err := mts.GetSubscribedURLs()
+                       assert.NoError(t, err)
+                       assert.NotEmpty(t, urls)
+               }()
+               wg.Add(1)
+               go func() {
+                       defer wg.Done()
+                       info, err := mts.GetMetadataInfo("1")
+                       assert.NoError(t, err)
+                       assert.NotNil(t, info)
+               }()
+       }
+       wg.Wait()
+}
diff --git a/metadata/metadata_test.go b/metadata/metadata_test.go
index 2c8de5b27..da4446e29 100644
--- a/metadata/metadata_test.go
+++ b/metadata/metadata_test.go
@@ -18,6 +18,7 @@
 package metadata
 
 import (
+       "sync"
        "testing"
 )
 
@@ -179,3 +180,36 @@ func TestGetMetadataService(t *testing.T) {
                })
        }
 }
+
+func TestAddServiceConcurrent(t *testing.T) {
+       var wg sync.WaitGroup
+       for i := 0; i < 20; i++ {
+               registryId := "concurrent-reg"
+               wg.Add(1)
+               go func(idx int) {
+                       defer wg.Done()
+                       url := common.NewURLWithOptions(
+                               common.WithProtocol("dubbo"),
+                               common.WithParamsValue(constant.ApplicationKey, 
"dubbo"),
+                               
common.WithParamsValue(constant.ApplicationTagKey, "v1"),
+                       )
+                       AddService(registryId, url)
+               }(i)
+               wg.Add(1)
+               go func(idx int) {
+                       defer wg.Done()
+                       url := common.NewURLWithOptions(
+                               common.WithProtocol("dubbo"),
+                               common.WithParamsValue(constant.ApplicationKey, 
"dubbo"),
+                               
common.WithParamsValue(constant.ApplicationTagKey, "v1"),
+                       )
+                       AddSubscribeURL(registryId, url)
+               }(i)
+               wg.Add(1)
+               go func(idx int) {
+                       defer wg.Done()
+                       _ = GetMetadataInfo(registryId)
+               }(i)
+       }
+       wg.Wait()
+}
diff --git a/metadata/report/nacos/report_test.go 
b/metadata/report/nacos/report_test.go
index 108e330bb..1ce968772 100644
--- a/metadata/report/nacos/report_test.go
+++ b/metadata/report/nacos/report_test.go
@@ -171,7 +171,7 @@ func newNacosMetadataReport(f fields) *nacosMetadataReport {
 }
 
 func Test_nacosMetadataReport_GetAppMetadata(t *testing.T) {
-       mi := info.MetadataInfo{
+       mi := &info.MetadataInfo{
                App: "GetAppMetadata",
        }
        data, _ := json.Marshal(mi)
@@ -198,7 +198,7 @@ func Test_nacosMetadataReport_GetAppMetadata(t *testing.T) {
                                application: "dubbo",
                                revision:    "revision",
                        },
-                       want:    &mi,
+                       want:    mi,
                        wantErr: false,
                },
        }
diff --git a/metadata/report_instance.go b/metadata/report_instance.go
index 55cf516df..4d2354b87 100644
--- a/metadata/report_instance.go
+++ b/metadata/report_instance.go
@@ -19,6 +19,7 @@ package metadata
 
 import (
        "sort"
+       "sync"
        "time"
 )
 
@@ -39,7 +40,8 @@ import (
 )
 
 var (
-       instances = make(map[string]report.MetadataReport)
+       instances   = make(map[string]report.MetadataReport)
+       instancesMu sync.RWMutex
 )
 
 // ClearMetadataReportInstances resets the package-level instances map.
@@ -54,7 +56,10 @@ func addMetadataReport(registryId string, url *common.URL) 
error {
                logger.Warnf("[Metadata] no metadata report factory of protocol 
%s found, please check if the metadata report factory is imported", 
url.Protocol)
                return nil
        }
-       instances[registryId] = &DelegateMetadataReport{instance: 
fac.CreateMetadataReport(url)}
+       mr := &DelegateMetadataReport{instance: fac.CreateMetadataReport(url)}
+       instancesMu.Lock()
+       instances[registryId] = mr
+       instancesMu.Unlock()
        return nil
 }
 
@@ -63,6 +68,9 @@ func addMetadataReport(registryId string, url *common.URL) 
error {
 // it falls back to the lexicographically first registry id so the selection
 // is always stable across calls.
 func GetMetadataReport() report.MetadataReport {
+       instancesMu.RLock()
+       defer instancesMu.RUnlock()
+
        if r, ok := instances[constant.DefaultKey]; ok {
                return r
        }
@@ -89,6 +97,8 @@ func GetMetadataReportByRegistry(registry string) 
report.MetadataReport {
        if len(registry) == 0 {
                return GetMetadataReport()
        }
+       instancesMu.RLock()
+       defer instancesMu.RUnlock()
        if r, ok := instances[registry]; ok {
                return r
        }
@@ -101,6 +111,8 @@ func GetMetadataReportByRegistry(registry string) 
report.MetadataReport {
 }
 
 func GetMetadataReports() []report.MetadataReport {
+       instancesMu.RLock()
+       defer instancesMu.RUnlock()
        reports := make([]report.MetadataReport, len(instances))
        index := 0
        for _, r := range instances {
diff --git 
a/registry/servicediscovery/service_instances_changed_listener_impl.go 
b/registry/servicediscovery/service_instances_changed_listener_impl.go
index 427895aeb..8cb28b6d8 100644
--- a/registry/servicediscovery/service_instances_changed_listener_impl.go
+++ b/registry/servicediscovery/service_instances_changed_listener_impl.go
@@ -144,7 +144,7 @@ func (lstn *ServiceInstancesChangedListenerImpl) OnEvent(e 
observer.Event) error
                                continue
                        }
                        instance.SetServiceMetadata(metadataInfo)
-                       for _, service := range metadataInfo.Services {
+                       for _, service := range metadataInfo.GetServices() {
                                matchKey := service.GetMatchKey()
                                if serviceToRevisionServices[matchKey] == nil {
                                        serviceToRevisionServices[matchKey] = 
make(map[string]*info.ServiceInfo)
@@ -215,8 +215,11 @@ func toInstanceServiceURLs(instance 
registry.ServiceInstance, serviceInfo *info.
 
 // AddListenerAndNotify add notify listener and notify to listen service event
 func (lstn *ServiceInstancesChangedListenerImpl) 
AddListenerAndNotify(serviceKey string, notify registry.NotifyListener) {
+       lstn.mutex.Lock()
        lstn.listeners[serviceKey] = notify
        urls := lstn.serviceUrls[serviceKey]
+       lstn.mutex.Unlock()
+
        for _, url := range urls {
                notify.Notify(&registry.ServiceEvent{
                        Action:  remoting.EventTypeAdd,
@@ -227,6 +230,8 @@ func (lstn *ServiceInstancesChangedListenerImpl) 
AddListenerAndNotify(serviceKey
 
 // RemoveListener remove notify listener
 func (lstn *ServiceInstancesChangedListenerImpl) RemoveListener(serviceKey 
string) {
+       lstn.mutex.Lock()
+       defer lstn.mutex.Unlock()
        delete(lstn.listeners, serviceKey)
 }
 
diff --git 
a/registry/servicediscovery/service_instances_changed_listener_impl_test.go 
b/registry/servicediscovery/service_instances_changed_listener_impl_test.go
index 22f7582ae..b0e3fb5b7 100644
--- a/registry/servicediscovery/service_instances_changed_listener_impl_test.go
+++ b/registry/servicediscovery/service_instances_changed_listener_impl_test.go
@@ -375,6 +375,18 @@ func (c *capturingNotifyListener) NotifyAll(events 
[]*registry.ServiceEvent, cal
        }
 }
 
+func TestServiceInstancesChangedListenerRemoveListener(t *testing.T) {
+       listener := NewServiceInstancesChangedListener(testApp, 
constant.DefaultKey, 
gxset.NewSet(testApp)).(*ServiceInstancesChangedListenerImpl)
+       notify := &capturingNotifyListener{}
+       key := common.MatchKey(testInterface, constant.TriProtocol)
+
+       listener.AddListenerAndNotify(key, notify)
+       require.Len(t, listener.listeners, 1)
+
+       listener.RemoveListener(key)
+       require.Empty(t, listener.listeners)
+}
+
 func TestGetMetadataInfo_CacheKeyFormat(t *testing.T) {
        // Ensure cache is initialized (normally done by 
NewServiceInstancesChangedListener)
        _ = NewServiceInstancesChangedListener(testApp, constant.DefaultKey, 
gxset.NewSet(testApp))

Reply via email to