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 c79f6166f fix:cache manager race #3569 (#3601)
c79f6166f is described below

commit c79f6166fda32c3047cb1444d9ccff4476f85630
Author: xiaobaicai66695 <[email protected]>
AuthorDate: Thu Aug 20 13:26:05 2026 +0800

    fix:cache manager race #3569 (#3601)
    
    * fix: guard service discovery cache manager access
    
    * test: verify cache manager dump lifecycle
    
    * style: modernize cache manager concurrency tests
    
    * test: reduce cache manager test complexity
    
    * test: verify cache manager get all snapshot
    
    * test: cover cache manager stop dump deadlock
---
 registry/servicediscovery/store/cache_manager.go   |  69 +++--
 .../servicediscovery/store/cache_manager_test.go   | 283 +++++++++++++++++++++
 2 files changed, 334 insertions(+), 18 deletions(-)

diff --git a/registry/servicediscovery/store/cache_manager.go 
b/registry/servicediscovery/store/cache_manager.go
index 2655a2772..0be57997e 100644
--- a/registry/servicediscovery/store/cache_manager.go
+++ b/registry/servicediscovery/store/cache_manager.go
@@ -35,9 +35,10 @@ type CacheManager struct {
        cacheFile    string        // The file path where the cache is stored
        dumpInterval time.Duration // The duration after which the cache dump
        stop         chan struct{} // Channel used to stop the cache expiration 
routine
+       done         chan struct{} // Channel closed after the cache expiration 
routine stops
        cache        *lru.Cache    // The LRU cache implementation
        lock         sync.Mutex
-       enableDump   bool
+       stopOnce     sync.Once
 }
 
 type Item struct {
@@ -45,6 +46,8 @@ type Item struct {
        Value any
 }
 
+var cacheManagerBeforeDumpCache func()
+
 // NewCacheManager creates a new CacheManager instance.
 // It initializes the cache manager with the provided parameters and starts a 
routine for cache dumping.
 func NewCacheManager(name, cacheFile string, dumpInterval time.Duration, 
maxCacheSize int, enableDump bool) (*CacheManager, error) {
@@ -53,7 +56,7 @@ func NewCacheManager(name, cacheFile string, dumpInterval 
time.Duration, maxCach
                cacheFile:    cacheFile,
                dumpInterval: dumpInterval,
                stop:         make(chan struct{}),
-               enableDump:   enableDump,
+               done:         make(chan struct{}),
        }
        cache, err := lru.New(maxCacheSize)
        if err != nil {
@@ -70,6 +73,8 @@ func NewCacheManager(name, cacheFile string, dumpInterval 
time.Duration, maxCach
 
        if enableDump {
                cm.runDumpTask()
+       } else {
+               close(cm.done)
        }
 
        return cm, nil
@@ -77,31 +82,56 @@ func NewCacheManager(name, cacheFile string, dumpInterval 
time.Duration, maxCach
 
 // Get retrieves the value associated with the given key from the cache.
 func (cm *CacheManager) Get(key string) (any, bool) {
+       cm.lock.Lock()
+       defer cm.lock.Unlock()
        return cm.cache.Get(key)
 }
 
 // Set sets the value associated with the given key in the cache.
 func (cm *CacheManager) Set(key string, value any) {
+       cm.lock.Lock()
+       defer cm.lock.Unlock()
        cm.cache.Add(key, value)
 }
 
 // Delete removes the value associated with the given key from the cache.
 func (cm *CacheManager) Delete(key string) {
+       cm.lock.Lock()
+       defer cm.lock.Unlock()
        cm.cache.Remove(key)
 }
 
 // GetAll returns all the key-value pairs in the cache.
 func (cm *CacheManager) GetAll() map[string]any {
+       cm.lock.Lock()
+       defer cm.lock.Unlock()
+
+       return cm.getAllLocked()
+}
+
+func (cm *CacheManager) getAllLocked() map[string]any {
        keys := cm.cache.Keys()
 
-       result := make(map[string]any)
+       result := make(map[string]any, len(keys))
        for _, k := range keys {
-               result[k.(string)], _ = cm.cache.Get(k)
+               key, ok := k.(string)
+               if !ok {
+                       continue
+               }
+               if value, ok := cm.cache.Get(k); ok {
+                       result[key] = value
+               }
        }
 
        return result
 }
 
+func (cm *CacheManager) len() int {
+       cm.lock.Lock()
+       defer cm.lock.Unlock()
+       return cm.cache.Len()
+}
+
 // loadCache loads the cache from the cache file.
 func (cm *CacheManager) loadCache() error {
        cf, err := os.Open(cm.cacheFile)
@@ -121,7 +151,7 @@ func (cm *CacheManager) loadCache() error {
                        return err
                }
                // Add the loaded keys to the front of the LRU list
-               cm.cache.Add(it.Key, it.Value)
+               cm.Set(it.Key, it.Value)
        }
 
        return nil
@@ -129,10 +159,6 @@ func (cm *CacheManager) loadCache() error {
 
 // dumpCache dumps the cache to the cache file.
 func (cm *CacheManager) dumpCache() error {
-
-       cm.lock.Lock()
-       defer cm.lock.Unlock()
-
        items := cm.GetAll()
 
        file, err := os.Create(cm.cacheFile)
@@ -158,18 +184,24 @@ func (cm *CacheManager) dumpCache() error {
 func (cm *CacheManager) runDumpTask() {
        go func() {
                ticker := time.NewTicker(cm.dumpInterval)
+               defer func() {
+                       ticker.Stop()
+                       close(cm.done)
+               }()
                for {
                        select {
                        case <-ticker.C:
+                               if cacheManagerBeforeDumpCache != nil {
+                                       cacheManagerBeforeDumpCache()
+                               }
                                // Dump the cache to the file
                                if err := cm.dumpCache(); err != nil {
                                        // Handle error
                                        
logger.Warnf("[Registry][ServiceDiscovery] failed to dump cache, err=%v", err)
                                } else {
-                                       
logger.Infof("[Registry][ServiceDiscovery] dumping [%s] caches, latest 
entries=%d", cm.name, cm.cache.Len())
+                                       
logger.Infof("[Registry][ServiceDiscovery] dumping [%s] caches, latest 
entries=%d", cm.name, cm.len())
                                }
                        case <-cm.stop:
-                               ticker.Stop()
                                return
                        }
                }
@@ -177,18 +209,19 @@ func (cm *CacheManager) runDumpTask() {
 }
 
 func (cm *CacheManager) StopDump() {
-       cm.lock.Lock()
-       defer cm.lock.Unlock()
-       if cm.enableDump {
-               cm.stop <- struct{}{} // Stop the cache dump routine
-               cm.enableDump = false
-       }
+       cm.stopOnce.Do(func() {
+               close(cm.stop)
+       })
+       <-cm.done
 }
 
 // destroy stops the cache dump routine, clears the cache and removes the 
cache file.
 func (cm *CacheManager) destroy() {
-       cm.StopDump()    // Stop the cache dump routine
+       cm.StopDump() // Stop the cache dump routine
+
+       cm.lock.Lock()
        cm.cache.Purge() // Clear the cache
+       cm.lock.Unlock()
 
        // Delete the cache file if it exists
        if _, err := os.Stat(cm.cacheFile); err == nil {
diff --git a/registry/servicediscovery/store/cache_manager_test.go 
b/registry/servicediscovery/store/cache_manager_test.go
index 36541d7ea..8cd68f1d3 100644
--- a/registry/servicediscovery/store/cache_manager_test.go
+++ b/registry/servicediscovery/store/cache_manager_test.go
@@ -18,6 +18,11 @@
 package store
 
 import (
+       "fmt"
+       "os"
+       "path/filepath"
+       "runtime"
+       "sync"
        "testing"
        "time"
 )
@@ -150,3 +155,281 @@ func TestMetaInfoCacheManager(t *testing.T) {
        cm2.destroy()
        cm.destroy() // clear cache file
 }
+
+func TestCacheManagerConcurrentAccess(t *testing.T) {
+       cacheFile := filepath.Join(t.TempDir(), "race_cache")
+       cm, err := NewCacheManager("raceTest", cacheFile, time.Millisecond, 32, 
true)
+       if err != nil {
+               t.Fatalf("failed to create cache manager: %v", err)
+       }
+       defer cm.destroy()
+
+       runConcurrentCacheAccess(cm)
+       waitForCacheDump(t, cacheFile)
+       cm.StopDump()
+
+       loaded, err := NewCacheManager("raceTestReloaded", cacheFile, 
time.Hour, 32, false)
+       if err != nil {
+               t.Fatalf("failed to reload cache manager: %v", err)
+       }
+       defer loaded.destroy()
+
+       assertCacheEntries(t, loaded.GetAll())
+}
+
+func runConcurrentCacheAccess(cm *CacheManager) {
+       var wg sync.WaitGroup
+       for i := range 16 {
+               wg.Add(1)
+               go func(worker int) {
+                       defer wg.Done()
+                       exerciseCacheManager(cm, worker)
+               }(i)
+       }
+       wg.Wait()
+}
+
+func exerciseCacheManager(cm *CacheManager, worker int) {
+       for j := range 500 {
+               key := fmt.Sprintf("key-%d", j%64)
+               cm.Set(key, fmt.Sprintf("value-%d-%d", worker, j))
+               cm.Get(key)
+               if j%3 == 0 {
+                       cm.Delete(fmt.Sprintf("key-%d", (j+worker)%64))
+               }
+               if j%7 == 0 {
+                       cm.GetAll()
+               }
+       }
+}
+
+func waitForCacheDump(t *testing.T, cacheFile string) {
+       t.Helper()
+       deadline := time.Now().Add(5 * time.Second)
+       for {
+               info, statErr := os.Stat(cacheFile)
+               if statErr == nil && info.Size() > 0 {
+                       break
+               }
+               if time.Now().After(deadline) {
+                       t.Fatalf("cache dump was not created: %v", statErr)
+               }
+               time.Sleep(time.Millisecond)
+       }
+}
+
+func assertCacheEntries(t *testing.T, items map[string]any) {
+       t.Helper()
+       if len(items) == 0 {
+               t.Fatal("reloaded cache dump contained no entries")
+       }
+       for key, value := range items {
+               if value == nil {
+                       t.Fatalf("reloaded cache entry %q contained a nil 
value", key)
+               }
+               if _, ok := value.(string); !ok {
+                       t.Fatalf("reloaded cache entry %q had unexpected type 
%T", key, value)
+               }
+       }
+}
+
+func TestCacheManagerGetAllReturnsAtomicSnapshotDuringReplacement(t 
*testing.T) {
+       previousGOMAXPROCS := runtime.GOMAXPROCS(4)
+       defer runtime.GOMAXPROCS(previousGOMAXPROCS)
+
+       cm, err := NewCacheManager("snapshotTest", filepath.Join(t.TempDir(), 
"snapshot_cache"), time.Hour, 1, false)
+       if err != nil {
+               t.Fatalf("failed to create cache manager: %v", err)
+       }
+       defer cm.destroy()
+
+       cm.Set("a", "value-a")
+
+       stop, errCh, waitReaders := startSnapshotReaders(cm, 8)
+       replaceCapacityOneEntries(cm, 100000)
+       close(stop)
+       waitReaders()
+
+       if msg := snapshotError(errCh); msg != "" {
+               t.Fatal(msg)
+       }
+}
+
+func startSnapshotReaders(cm *CacheManager, readers int) (chan struct{}, chan 
string, func()) {
+       start := make(chan struct{})
+       stop := make(chan struct{})
+       errCh := make(chan string, 1)
+
+       var wg sync.WaitGroup
+       for range readers {
+               wg.Go(func() {
+                       readSnapshotsUntilStopped(cm, start, stop, errCh)
+               })
+       }
+       close(start)
+
+       return stop, errCh, wg.Wait
+}
+
+func readSnapshotsUntilStopped(cm *CacheManager, start, stop <-chan struct{}, 
errCh chan<- string) {
+       <-start
+       for !snapshotStopped(stop) {
+               if msg := validateCapacityOneSnapshot(cm.GetAll()); msg != "" {
+                       reportSnapshotError(errCh, msg)
+                       return
+               }
+       }
+}
+
+func snapshotStopped(stop <-chan struct{}) bool {
+       select {
+       case <-stop:
+               return true
+       default:
+               return false
+       }
+}
+
+func replaceCapacityOneEntries(cm *CacheManager, replacements int) {
+       for i := range replacements {
+               if i%2 == 0 {
+                       cm.Set("b", "value-b")
+               } else {
+                       cm.Set("a", "value-a")
+               }
+       }
+}
+
+func validateCapacityOneSnapshot(items map[string]any) string {
+       if len(items) != 1 {
+               return fmt.Sprintf("GetAll returned %d entries during 
capacity-1 replacement: %#v", len(items), items)
+       }
+
+       if value, ok := items["a"]; ok {
+               return validateSnapshotEntry("a", value, "value-a")
+       }
+       if value, ok := items["b"]; ok {
+               return validateSnapshotEntry("b", value, "value-b")
+       }
+       return fmt.Sprintf("GetAll returned unexpected snapshot: %#v", items)
+}
+
+func validateSnapshotEntry(key string, value, expected any) string {
+       if value == expected {
+               return ""
+       }
+       return fmt.Sprintf("GetAll returned unexpected entry %q=%#v", key, 
value)
+}
+
+func snapshotError(errCh <-chan string) string {
+       select {
+       case msg := <-errCh:
+               return msg
+       default:
+               return ""
+       }
+}
+
+func reportSnapshotError(errCh chan<- string, msg string) {
+       select {
+       case errCh <- msg:
+       default:
+       }
+}
+
+func TestCacheManagerConcurrentStopDump(t *testing.T) {
+       tests := []struct {
+               name       string
+               enableDump bool
+       }{
+               {name: "enabled", enableDump: true},
+               {name: "disabled", enableDump: false},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       cm, err := NewCacheManager("stopTest", 
filepath.Join(t.TempDir(), "stop_cache"), time.Hour, 8, tt.enableDump)
+                       if err != nil {
+                               t.Fatalf("failed to create cache manager: %v", 
err)
+                       }
+                       defer cm.destroy()
+
+                       const callers = 64
+                       start := make(chan struct{})
+                       var wg sync.WaitGroup
+                       for range callers {
+                               wg.Go(func() {
+                                       <-start
+                                       cm.StopDump()
+                               })
+                       }
+                       close(start)
+
+                       done := make(chan struct{})
+                       go func() {
+                               wg.Wait()
+                               close(done)
+                       }()
+
+                       select {
+                       case <-done:
+                       case <-time.After(2 * time.Second):
+                               t.Fatal("concurrent StopDump calls did not 
complete")
+                       }
+
+                       cm.StopDump()
+               })
+       }
+}
+
+func TestCacheManagerStopDumpCompletesAfterDumpTickSelected(t *testing.T) {
+       dumpSelected := make(chan struct{})
+       releaseDump := make(chan struct{})
+       var selectedOnce sync.Once
+       var releaseOnce sync.Once
+       release := func() {
+               releaseOnce.Do(func() {
+                       close(releaseDump)
+               })
+       }
+
+       cacheManagerBeforeDumpCache = func() {
+               selectedOnce.Do(func() {
+                       close(dumpSelected)
+               })
+               <-releaseDump
+       }
+       t.Cleanup(func() {
+               release()
+               cacheManagerBeforeDumpCache = nil
+       })
+
+       cm, err := NewCacheManager("stopTickTest", filepath.Join(t.TempDir(), 
"stop_tick_cache"), 100*time.Millisecond, 8, true)
+       if err != nil {
+               t.Fatalf("failed to create cache manager: %v", err)
+       }
+       cm.Set("key", "value")
+
+       select {
+       case <-dumpSelected:
+       case <-time.After(2 * time.Second):
+               t.Fatal("dump task did not select ticker")
+       }
+
+       done := make(chan struct{})
+       cm.lock.Lock()
+       go func() {
+               cm.StopDump()
+               close(done)
+       }()
+       time.Sleep(20 * time.Millisecond)
+
+       release()
+       cm.lock.Unlock()
+
+       select {
+       case <-done:
+       case <-time.After(2 * time.Second):
+               t.Fatal("StopDump did not complete after selected dump was 
released")
+       }
+}

Reply via email to