-
Notifications
You must be signed in to change notification settings - Fork 1k
cubemaster: fix the data races in localcache #1376
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 2 commits
b0974e1
cbc6f8e
74b7ccc
fca97d1
4e50c74
68456e9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,6 +65,8 @@ type LocalCache struct { | |
| localCacheConfig *LocalCacheConfig | ||
| sharedCalls util.SharedCalls | ||
| consecutiveFailNum int64 | ||
| expiredUse atomic.Bool | ||
| destroyOnce sync.Once | ||
| } | ||
|
|
||
| func NewCache(name string, loader LoaderFunc, localCacheConfig *LocalCacheConfig) *LocalCache { | ||
|
|
@@ -77,6 +79,7 @@ func NewCache(name string, loader LoaderFunc, localCacheConfig *LocalCacheConfig | |
| localCache.loadFile(localCacheConfig.LoadFileName) | ||
| } | ||
| localCache.localCacheConfig = localCache.SetupConfig(localCacheConfig) | ||
| localCache.expiredUse.Store(localCache.localCacheConfig.ExpiredUse) | ||
|
|
||
| localCache.chShrinkCache = make(chan bool, 1) | ||
| localCache.chCacheExit = make(chan bool) | ||
|
|
@@ -99,36 +102,39 @@ func NewCache(name string, loader LoaderFunc, localCacheConfig *LocalCacheConfig | |
| } | ||
|
|
||
| func (localCache *LocalCache) Destroy() { | ||
| if localCache != nil { | ||
| if localCache == nil { | ||
| return | ||
| } | ||
| localCache.destroyOnce.Do(func() { | ||
| CubeLog.Infof("LruCache(%s) Destroy", localCache.name) | ||
| if localCache.chCacheExit != nil { | ||
| if localCache.localCacheConfig.OpenCacheFile { | ||
| localCache.saveFile(localCache.localCacheConfig.LoadFileName) | ||
| } | ||
| localCache.cache.Flush() | ||
| close(localCache.chCacheExit) | ||
| localCache.waitGroup.Wait() | ||
| localCache.chCacheExit = nil | ||
| if localCache.chCacheExit == nil { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dead code now: after this PR nothing assigns |
||
| return | ||
| } | ||
| } | ||
| if localCache.localCacheConfig.OpenCacheFile { | ||
| localCache.saveFile(localCache.localCacheConfig.LoadFileName) | ||
| } | ||
| localCache.cache.Flush() | ||
| close(localCache.chCacheExit) | ||
| localCache.waitGroup.Wait() | ||
| }) | ||
| } | ||
|
|
||
| func (localCache *LocalCache) Get(ctx context.Context, key string) (interface{}, bool, error) { | ||
| item, found := localCache.cache.Get(key) | ||
| if found { | ||
| element := item.(*list.Element) | ||
|
|
||
| localCache.Lock() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The PR description says |
||
| itm := element.Value.(*util.CacheValue) | ||
| localCache.valueList.MoveToBack(element) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Behavior change vs. the PR description. The description says "the LRU effect is the same — exactly one MoveToBack per hit, on access", but on the default config ( |
||
| localCache.Unlock() | ||
|
|
||
| if time.Now().Add(-itm.Expired).After(time.Unix(itm.LastAccess, 0)) { | ||
|
|
||
| if !localCache.localCacheConfig.ExpiredUse { | ||
| if !localCache.expiredUse.Load() { | ||
| r, f, err := localCache.loadAndRefresh(ctx, key) | ||
|
|
||
| if err != nil && localCache.localCacheConfig.DemotionExpiredUse { | ||
|
|
||
| localCache.Lock() | ||
| localCache.valueList.MoveToBack(element) | ||
| localCache.Unlock() | ||
| return itm.Value, true, nil | ||
| } | ||
|
|
||
|
|
@@ -142,9 +148,6 @@ func (localCache *LocalCache) Get(ctx context.Context, key string) (interface{}, | |
| localCache.asyncRefresh(ctx, key) | ||
| } | ||
|
|
||
| localCache.Lock() | ||
| localCache.valueList.MoveToBack(element) | ||
| localCache.Unlock() | ||
| return itm.Value, true, nil | ||
| } | ||
|
|
||
|
|
@@ -159,14 +162,17 @@ func (localCache *LocalCache) put(key string, val interface{}, expired time.Dura | |
| if item, found := localCache.cache.Get(key); found { | ||
| element := item.(*list.Element) | ||
| localCache.Lock() | ||
| prev := element.Value.(*util.CacheValue) | ||
| next := &util.CacheValue{ | ||
| Key: prev.Key, | ||
| Value: val, | ||
| LastAccess: time.Now().Unix(), | ||
| Expired: expired} | ||
| element.Value = next | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is a newly-introduced data race. Before this PR, Fix suggestion: snapshot the |
||
| localCache.valueList.MoveToBack(element) | ||
| localCache.Unlock() | ||
| itm := element.Value.(*util.CacheValue) | ||
| atomic.AddInt64(&localCache.curCacheSize, -itm.Size()) | ||
| itm.Value = val | ||
| itm.Expired = expired | ||
| itm.LastAccess = time.Now().Unix() | ||
| atomic.AddInt64(&localCache.curCacheSize, itm.Size()) | ||
| atomic.AddInt64(&localCache.curCacheSize, -prev.Size()) | ||
| atomic.AddInt64(&localCache.curCacheSize, next.Size()) | ||
| } else { | ||
| itm := &util.CacheValue{ | ||
| Key: key, | ||
|
|
@@ -180,7 +186,7 @@ func (localCache *LocalCache) put(key string, val interface{}, expired time.Dura | |
| localCache.cache.Set(key, element, -1) | ||
| } | ||
|
|
||
| if localCache.curCacheSize >= localCache.localCacheConfig.HighCacheSize { | ||
| if atomic.LoadInt64(&localCache.curCacheSize) >= localCache.localCacheConfig.HighCacheSize { | ||
| localCache.chShrinkCache <- true | ||
| } | ||
| } | ||
|
|
@@ -198,7 +204,7 @@ func (localCache *LocalCache) loadAndRefresh(ctx context.Context, key string) (i | |
| CubeLog.Errorf("Cache LoadAndRefresh Error:%s, %v, %s", key, found, err) | ||
| return nil, err | ||
| } else { | ||
| localCache.consecutiveFailNum = 0 | ||
| atomic.StoreInt64(&localCache.consecutiveFailNum, 0) | ||
| } | ||
|
|
||
| if !found { | ||
|
|
@@ -305,12 +311,12 @@ func (localCache *LocalCache) errStrategy() { | |
| int64(localCache.localCacheConfig.MaxConsecutiveFailNum) { | ||
|
|
||
| if localCache.localCacheConfig.DemotionExpiredUse { | ||
| localCache.localCacheConfig.ExpiredUse = true | ||
| localCache.expiredUse.Store(true) | ||
| } | ||
| } else { | ||
|
|
||
| if localCache.localCacheConfig.DemotionExpiredUse && localCache.localCacheConfig.ExpiredUse { | ||
| localCache.localCacheConfig.ExpiredUse = false | ||
| if localCache.localCacheConfig.DemotionExpiredUse && localCache.expiredUse.Load() { | ||
| localCache.expiredUse.Store(false) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| // Copyright (c) 2024 Tencent Inc. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| // | ||
|
|
||
| package localcache | ||
|
|
||
| import ( | ||
| "context" | ||
| "sync" | ||
| "sync/atomic" | ||
| "testing" | ||
| "time" | ||
| ) | ||
|
|
||
| func TestConcurrentGetAndRefreshOnSameKey(t *testing.T) { | ||
| var loads int64 | ||
| localCache := NewCache("race-probe", | ||
| func(ctx context.Context, key string) (interface{}, bool, error) { | ||
| atomic.AddInt64(&loads, 1) | ||
| return RandString(16), true, nil | ||
| }, | ||
| &LocalCacheConfig{ | ||
| LowCacheSize: 1000000, | ||
| HighCacheSize: 2000000, | ||
| Expired: time.Millisecond, | ||
| AsyncRefreshBefore: time.Millisecond, | ||
| MaxAsyncRefreshNum: 100, | ||
| ExpiredUse: true, | ||
| }) | ||
| defer localCache.Destroy() | ||
|
|
||
| ctx := context.Background() | ||
| const readers = 32 | ||
| const iterations = 300 | ||
|
|
||
| var wg sync.WaitGroup | ||
| wg.Add(readers) | ||
| for i := 0; i < readers; i++ { | ||
| go func() { | ||
| defer wg.Done() | ||
| for j := 0; j < iterations; j++ { | ||
| v, found, err := localCache.Get(ctx, "hot-key") | ||
| if err != nil { | ||
| t.Errorf("Get: %v", err) | ||
| return | ||
| } | ||
| if found { | ||
| if _, ok := v.(string); !ok { | ||
| t.Errorf("Get returned %T, want string; a torn interface read", v) | ||
| return | ||
| } | ||
| } | ||
| } | ||
| }() | ||
| } | ||
| wg.Wait() | ||
|
|
||
| if atomic.LoadInt64(&loads) == 0 { | ||
| t.Fatal("loader never ran; the test did not exercise the refresh path") | ||
| } | ||
| } | ||
|
|
||
| func TestDestroyIsIdempotent(t *testing.T) { | ||
| localCache := NewCache("destroy-probe", | ||
| func(ctx context.Context, key string) (interface{}, bool, error) { | ||
| return "v", true, nil | ||
| }, | ||
| &LocalCacheConfig{LowCacheSize: 1000, HighCacheSize: 2000, Expired: time.Minute}) | ||
|
|
||
| localCache.Destroy() | ||
| localCache.Destroy() | ||
| } | ||
|
|
||
| func TestConcurrentDestroyDoesNotRaceWithBackgroundLoops(t *testing.T) { | ||
| localCache := NewCache("destroy-race-probe", | ||
| func(ctx context.Context, key string) (interface{}, bool, error) { | ||
| return "v", true, nil | ||
| }, | ||
| &LocalCacheConfig{LowCacheSize: 1000, HighCacheSize: 2000, Expired: time.Minute}) | ||
|
|
||
| var wg sync.WaitGroup | ||
| wg.Add(4) | ||
| for i := 0; i < 4; i++ { | ||
| go func() { | ||
| defer wg.Done() | ||
| localCache.Destroy() | ||
| }() | ||
| } | ||
| wg.Wait() | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SetupConfigdeliberately guards a nil config (if localCacheConfig == nil { return nil }), solocalCache.localCacheConfigcan be nil here and this new unconditionallocalCacheConfig.ExpiredUsedereference turns a nil config into a panic at construction time — previously the panic was deferred to the firstGeton an expired entry. No current caller passes nil, so this is latent rather than a live bug, but the new code bypasses the nil guard thatSetupConfigprovides. ConsiderlocalCache.expiredUse.Store(localCacheConfig != nil && localCacheConfig.ExpiredUse)or an explicit nil check.