AlexStocks commented on issue #3246:
URL: https://github.com/apache/dubbo-go/issues/3246#issuecomment-4059646934

   ## Concrete Fix Proposals (with code)
   
   Below are precise, copy-paste-ready fixes for each leak point. Ordered by 
priority.
   
   ---
   
   ### Fix 1 (P0): `registryProtocol.Destroy()` — clean `overrideListeners` and 
`serviceConfigurationListeners`
   
   **Current code** (`registry/protocol/protocol.go:457-496`):
   
   ```go
   func (proto *registryProtocol) Destroy() {
       proto.bounds.Range(func(key, value any) bool {
           exporter := value.(*exporterChangeableWrapper)
           // ... UnRegister + async UnExport ...
           return true
       })
       proto.bounds = &sync.Map{}
       proto.registries = &sync.Map{}
       // overrideListeners and serviceConfigurationListeners are NEVER cleaned
   }
   ```
   
   **Fix** — add cleanup at the end of `Destroy()`:
   
   ```go
   func (proto *registryProtocol) Destroy() {
       // ... existing bounds cleanup ...
   
       // Clean override listeners to release originInvoker references
       proto.overrideListeners.Range(func(key, value any) bool {
           proto.overrideListeners.Delete(key)
           return true
       })
   
       // Clean service configuration listeners to release providerUrl 
references
       proto.serviceConfigurationListeners.Range(func(key, value any) bool {
           proto.serviceConfigurationListeners.Delete(key)
           return true
       })
   
       proto.bounds = &sync.Map{}
       proto.registries = &sync.Map{}
   }
   ```
   
   Also fix `reExport()` — delete old listener before storing new one:
   
   ```go
   // registry/protocol/protocol.go, in reExport() around L380-400
   // Before storing new listener, delete old one:
   func (proto *registryProtocol) reExport(invoker base.Invoker, newUrl 
*common.URL) {
       key := getCacheKey(invoker)
       // Delete old override listener if exists
       if oldListener, loaded := 
proto.overrideListeners.LoadAndDelete(getProviderUrl(invoker)); loaded {
           _ = oldListener // allow GC
       }
       // ... rest of reExport ...
   }
   ```
   
   ---
   
   ### Fix 2 (P1): Add `DeleteAttribute` and `ClearAttributes` to URL
   
   **Current code** (`common/url.go:583-597`):
   
   ```go
   func (c *URL) SetAttribute(key string, value any) {
       c.attributesLock.Lock()
       defer c.attributesLock.Unlock()
       if c.attributes == nil {
           c.attributes = make(map[string]any)
       }
       c.attributes[key] = value
   }
   
   func (c *URL) GetAttribute(key string) (any, bool) {
       c.attributesLock.RLock()
       defer c.attributesLock.RUnlock()
       v, ok := c.attributes[key]
       return v, ok
   }
   ```
   
   **Fix** — add two new methods right after `GetAttribute`:
   
   ```go
   // DeleteAttribute removes the attribute with the given key.
   // Use this in Destroy() paths to release references held by URL.
   func (c *URL) DeleteAttribute(key string) {
       c.attributesLock.Lock()
       defer c.attributesLock.Unlock()
       delete(c.attributes, key)
   }
   
   // ClearAttributes removes all attributes.
   // Use this when the URL is being destroyed and all references should be 
released.
   func (c *URL) ClearAttributes() {
       c.attributesLock.Lock()
       defer c.attributesLock.Unlock()
       c.attributes = nil
   }
   ```
   
   **Where to call them** — in every `Destroy()` that holds a URL:
   
   ```go
   // protocol/base/protocol.go, in BaseProtocol.Destroy() or each protocol's 
Destroy:
   func (ivk *BaseInvoker) Destroy() {
       // ... existing cleanup ...
       if ivk.url != nil {
           ivk.url.ClearAttributes()
       }
   }
   
   // registry/directory/directory.go, in RegistryDirectory.Destroy():
   func (dir *RegistryDirectory) Destroy() {
       dir.DoDestroy(func() {
           // ... existing cleanup ...
           invokers := dir.cacheInvokers
           dir.cacheInvokers = []protocolbase.Invoker{}
           for _, ivk := range invokers {
               ivk.GetURL().ClearAttributes() // release attribute references 
before Destroy
               ivk.Destroy()
           }
       })
   }
   ```
   
   ---
   
   ### Fix 3 (P1): `RegistryDirectory.Destroy()` — clean `cacheInvokersMap`
   
   **Current code** (`registry/directory/directory.go:549-573`):
   
   ```go
   func (dir *RegistryDirectory) Destroy() {
       dir.DoDestroy(func() {
           // ...
           invokers := dir.cacheInvokers
           dir.cacheInvokers = []protocolbase.Invoker{}
           for _, ivk := range invokers {
               ivk.Destroy()
           }
           // cacheInvokersMap (sync.Map) is NEVER cleaned!
       })
   }
   ```
   
   **Fix** — add cleanup right after the invoker loop:
   
   ```go
   func (dir *RegistryDirectory) Destroy() {
       dir.DoDestroy(func() {
           // ...
           invokers := dir.cacheInvokers
           dir.cacheInvokers = []protocolbase.Invoker{}
           for _, ivk := range invokers {
               ivk.Destroy()
           }
   
           // Clean cacheInvokersMap to release all invoker references
           dir.cacheInvokersMap.Range(func(key, _ any) bool {
               dir.cacheInvokersMap.Delete(key)
               return true
           })
   
           // Also nil out configurators to release Configurator -> URL 
references
           dir.configurators = nil
       })
   }
   ```
   
   ---
   
   ### Fix 4 (P1): `invokerBlackList` key mismatch — unify key generation
   
   **Root cause**: `SetInvokerUnhealthyStatus` uses `invoker.GetURL().Key()`, 
but `RemoveUrlKeyUnhealthyStatus` receives keys from `GetCacheInvokerMapKey()` 
which has a different format.
   
   **Current code** (`protocol/base/rpc_status.go:212-243`):
   
   ```go
   func SetInvokerUnhealthyStatus(invoker Invoker) {
       invokerBlackList.Store(invoker.GetURL().Key(), invoker)  // Key() format
   }
   
   func RemoveUrlKeyUnhealthyStatus(key string) {
       invokerBlackList.Delete(key)  // key comes from GetCacheInvokerMapKey() 
format
   }
   ```
   
   **Caller** (`registry/directory/directory.go:457`):
   
   ```go
   func (dir *RegistryDirectory) uncacheInvokerWithKey(key string) {
       // key = ServiceEvent.Key() = URL.GetCacheInvokerMapKey()
       protocolbase.RemoveUrlKeyUnhealthyStatus(key)
   }
   ```
   
   **Two-part fix:**
   
   Option A — Store only key string, not invoker object; unify key format:
   
   ```go
   // rpc_status.go
   
   // Change blacklist to store URL.Key() -> struct{} (not full invoker)
   // and add a separate method that accepts URL directly
   
   func SetInvokerUnhealthyStatus(invoker Invoker) {
       key := invoker.GetURL().Key()
       invokerBlackList.Store(key, struct{}{})  // don't store full invoker
       blackListCacheDirty.Store(true)
       logger.Infof("%.10s add to black list", key)
   }
   
   func RemoveInvokerUnhealthyStatus(invoker Invoker) {
       key := invoker.GetURL().Key()
       invokerBlackList.Delete(key)
       blackListCacheDirty.Store(true)
       logger.Infof("%.10s removed from black list", key)
   }
   
   // Keep RemoveUrlKeyUnhealthyStatus but convert key format
   func RemoveUrlKeyUnhealthyStatus(cacheKey string) {
       // Also try to find and delete by iterating (since key formats differ)
       invokerBlackList.Range(func(k, _ any) bool {
           if strings.Contains(k.(string), extractServiceKey(cacheKey)) {
               invokerBlackList.Delete(k)
               blackListCacheDirty.Store(true)
               return false // found, stop
           }
           return true
       })
   }
   ```
   
   Option B (simpler, recommended) — make `uncacheInvokerWithKey` pass the 
correct key:
   
   ```go
   // registry/directory/directory.go
   func (dir *RegistryDirectory) uncacheInvokerWithKey(key string) {
       logger.Debugf("%.10s will be uncached", key)
       if cacheInvoker, ok := dir.cacheInvokersMap.Load(key); ok {
           dir.cacheInvokersMap.Delete(key)
           invoker := cacheInvoker.(protocolbase.Invoker)
           // Use the invoker's own URL.Key() to remove from blacklist
           protocolbase.RemoveUrlKeyUnhealthyStatus(invoker.GetURL().Key())
           invoker.Destroy()
       }
   }
   ```
   
   ---
   
   ### Fix 5 (P1): `GetParams()` — return copy instead of internal reference
   
   **Current code** (`common/url.go:657-659`):
   
   ```go
   func (c *URL) GetParams() url.Values {
       return c.params
   }
   ```
   
   **Fix**:
   
   ```go
   // GetParams returns a deep copy of the URL parameters.
   // Callers can safely modify the returned map without affecting the URL.
   func (c *URL) GetParams() url.Values {
       c.paramsLock.RLock()
       defer c.paramsLock.RUnlock()
       if c.params == nil {
           return url.Values{}
       }
       copy := make(url.Values, len(c.params))
       for k, vs := range c.params {
           vsCopy := make([]string, len(vs))
           for i, v := range vs {
               vsCopy[i] = v
           }
           copy[k] = vsCopy
       }
       return copy
   }
   ```
   
   **Impact**: Callers that relied on mutating the returned map to affect the 
URL will break. Audit these callers:
   
   ```go
   // registry/base_configuration_listener.go:111 — currently mutates returned 
map
   override := url.GetParams()
   delete(override, constant.AnyhostKey)  // after fix: only deletes from copy, 
URL unchanged
   // FIX: if intent is to modify URL, use url.DelParam(constant.AnyhostKey) 
instead
   
   // config_center/configurator/override.go:88 — uses 
SetParams(configUrl.GetParams())
   // After fix: this still works correctly since SetParams deep-copies its 
input
   ```
   
   ---
   
   ### Fix 6 (P1): `configurators` — replace instead of append
   
   **Current code** (`registry/directory/directory.go:375-386`):
   
   ```go
   func (dir *RegistryDirectory) convertUrl(res *registry.ServiceEvent) 
*common.URL {
       ret := res.Service
       if ret.Protocol == constant.OverrideProtocol ||
           ret.GetParam(constant.CategoryKey, constant.DefaultCategory) == 
constant.ConfiguratorsCategory {
           dir.configurators = append(dir.configurators, 
extension.GetDefaultConfigurator(ret))
           ret = nil
       }
       // ...
   }
   ```
   
   **Fix** — follow the same pattern as `BaseConfigurationListener.Process()` 
(L88), which replaces the entire slice:
   
   ```go
   func (dir *RegistryDirectory) convertUrl(res *registry.ServiceEvent) 
*common.URL {
       ret := res.Service
       if ret.Protocol == constant.OverrideProtocol ||
           ret.GetParam(constant.CategoryKey, constant.DefaultCategory) == 
constant.ConfiguratorsCategory {
           // Replace configurator for this service key, not append
           newConfigurator := extension.GetDefaultConfigurator(ret)
           dir.replaceConfigurator(newConfigurator)
           ret = nil
       }
       // ...
   }
   
   // replaceConfigurator replaces existing configurator for the same URL key,
   // or appends if no matching configurator exists.
   func (dir *RegistryDirectory) replaceConfigurator(newConf 
config_center.Configurator) {
       newKey := newConf.GetUrl().Key()
       for i, existing := range dir.configurators {
           if existing.GetUrl().Key() == newKey {
               dir.configurators[i] = newConf
               return
           }
       }
       dir.configurators = append(dir.configurators, newConf)
   }
   ```
   
   ---
   
   ### Fix 7 (P2): `MergeURL` — add lock when reading `anotherUrl.attributes`
   
   **Current code** (`common/url.go:879-891`):
   
   ```go
   // merge attributes
   if mergedURL.attributes == nil {
       mergedURL.attributes = make(map[string]any, len(anotherUrl.attributes))
   }
   for attrK, attrV := range anotherUrl.attributes {  // NO LOCK on anotherUrl!
       if _, ok := mergedURL.GetAttribute(attrK); !ok {
           mergedURL.attributes[attrK] = attrV
       }
   }
   ```
   
   **Fix**:
   
   ```go
   // merge attributes
   anotherUrl.attributesLock.RLock()
   anotherAttrs := make(map[string]any, len(anotherUrl.attributes))
   for k, v := range anotherUrl.attributes {
       anotherAttrs[k] = v
   }
   anotherUrl.attributesLock.RUnlock()
   
   if len(anotherAttrs) > 0 {
       if mergedURL.attributes == nil {
           mergedURL.attributes = make(map[string]any, len(anotherAttrs))
       }
       for attrK, attrV := range anotherAttrs {
           if _, ok := mergedURL.GetAttribute(attrK); !ok {
               mergedURL.attributes[attrK] = attrV
           }
       }
   }
   ```
   
   ---
   
   ### Fix 8 (P2): `GetCacheInvokerMapKey` — avoid allocating temporary URL
   
   **Current code** (`common/url.go:418-428`):
   
   ```go
   func (c *URL) GetCacheInvokerMapKey() string {
       urlNew, _ := NewURL(c.PrimitiveURL)  // allocates entire URL just to 
read one param
       buildString := urlNew.Protocol + "://" + 
urlNew.GetParam(constant.HostnameKey, urlNew.Location) +
           "/" + c.GetParam(constant.InterfaceKey, c.Path) +
           "?" + constant.TimeStamp + "=" + 
c.GetParam(constant.RemoteTimestampKey, "") +
           "&" + constant.MeshClusterIDKey + "=" + 
c.GetParam(constant.MeshClusterIDKey, "")
       return buildString
   }
   ```
   
   **Fix** — read HostnameKey directly from params, no need for NewURL:
   
   ```go
   func (c *URL) GetCacheInvokerMapKey() string {
       hostname := c.GetParam(constant.HostnameKey, c.Location)
       return c.Protocol + "://" + hostname +
           "/" + c.GetParam(constant.InterfaceKey, c.Path) +
           "?" + constant.TimeStamp + "=" + 
c.GetParam(constant.RemoteTimestampKey, "") +
           "&" + constant.MeshClusterIDKey + "=" + 
c.GetParam(constant.MeshClusterIDKey, "")
   }
   ```
   
   If `PrimitiveURL` contains a different hostname than the parsed `Location`, 
it means the URL parsing was inconsistent. In that case, 
`c.GetParam(constant.HostnameKey, c.Location)` is the safer fallback anyway.
   
   ---
   
   ### Fix 9 (P2): `isMatched` — use `SetParam` instead of `AddParam`
   
   **Current code** (`registry/protocol/protocol.go:400`):
   
   ```go
   func isMatched(party *common.URL, url *common.URL) bool {
       // ...
       providerUrl.AddParam(constant.CategoryKey, 
constant.ConfiguratorsCategory)  // appends, accumulates
       // ...
   }
   ```
   
   **Fix**:
   
   ```go
   providerUrl.SetParam(constant.CategoryKey, constant.ConfiguratorsCategory)  
// overwrites, no accumulation
   ```
   
   ---
   
   ### Fix 10 (P2): `registryProtocol.Destroy()` — add else branch for async 
cleanup
   
   **Current code** (`registry/protocol/protocol.go:468-491`):
   
   ```go
   go func() {
       if configShutdown := config.GetShutDown(); configShutdown != nil {
           <-time.After(configShutdown.GetStepTimeout() + 
configShutdown.GetConsumerUpdateWaitTime())
           exporter.UnExport()
           proto.bounds.Delete(key)
           return
       }
       if shutdownConfRaw, ok := 
exporter.registerUrl.GetAttribute(constant.ShutdownConfigPrefix); ok {
           if shutdownConf, ok := shutdownConfRaw.(*global.ShutdownConfig); ok {
               // ... same pattern ...
               return
           }
       }
       // BUG: if both conditions fail, nothing happens — exporter leaks
   }()
   ```
   
   **Fix** — add else branch:
   
   ```go
   go func() {
       if configShutdown := config.GetShutDown(); configShutdown != nil {
           <-time.After(configShutdown.GetStepTimeout() + 
configShutdown.GetConsumerUpdateWaitTime())
           exporter.UnExport()
           proto.bounds.Delete(key)
           return
       }
       if shutdownConfRaw, ok := 
exporter.registerUrl.GetAttribute(constant.ShutdownConfigPrefix); ok {
           if shutdownConf, ok := shutdownConfRaw.(*global.ShutdownConfig); ok {
               // ... same pattern ...
               return
           }
       }
       // Fallback: no shutdown config found, clean up immediately
       logger.Warnf("No shutdown config found for exporter %s, cleaning up 
immediately", key)
       exporter.UnExport()
       proto.bounds.Delete(key)
   }()
   ```


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to