diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d790f05..2d53fdf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: - name: Setup uses: actions/setup-go@v4 with: - go-version: "1.17" + go-version: "1.20" - run: go version - name: Checkout diff --git a/FUTURE.md b/FUTURE.md index 3ea524d..6bbb58e 100644 --- a/FUTURE.md +++ b/FUTURE.md @@ -1,10 +1,14 @@ ## ✒ 未来版本的新特性 (Features in future versions) +### v0.6.x + +* [ ] 梳理代码,优化代码风格,精简部分代码和注释 +* [ ] 完善监控上报器,提供更多缓存信息查询的方法 + ### v0.5.x * [ ] ~~提供一个清空并设置全量值的方法,方便定时数据的全量替换~~ 目前还找不到一个合适的设计去加入这个功能,并且也不是非常刚需,通过业务手段可以处理,所以先不加 -* [ ] 完善监控上报器,提供更多缓存信息查询的方法 ### v0.4.x diff --git a/HISTORY.md b/HISTORY.md index 57da66d..6d48994 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,12 @@ ## ✒ 历史版本的特性介绍 (Features in old versions) +### v0.6.0-alpha + +> 此版本发布于 2024-01-13 + +* 受小徒弟的灵感激发,进行 loader 代码的调整 +* 把 cache 结构去掉,精简这部分设计 + ### v0.5.0 > 此版本发布于 2023-11-30 diff --git a/Makefile b/Makefile index aebba3b..77225c3 100644 --- a/Makefile +++ b/Makefile @@ -3,10 +3,10 @@ all: test bench test: - go test -cover ./... + go test -cover -count=1 -test.cpu=1 ./... bench: - go test -v -bench=. -benchtime=1s ./_examples/performance_test.go + go test -v ./_examples/performance_test.go -bench=. -benchtime=1s fmt: go fmt ./... \ No newline at end of file diff --git a/README.en.md b/README.en.md index 128fffd..a835b6b 100644 --- a/README.en.md +++ b/README.en.md @@ -2,7 +2,7 @@ [![Go Doc](_icons/godoc.svg)](https://pkg.go.dev/github.com/FishGoddess/cachego) [![License](_icons/license.svg)](https://www.apache.org/licenses/LICENSE-2.0.html) -[![License](_icons/coverage.svg)](_icons/coverage.svg) +[![Coverage](_icons/coverage.svg)](_icons/coverage.svg) ![Test](https://github.com/FishGoddess/cachego/actions/workflows/test.yml/badge.svg) **cachego** is an api friendly memory-based cache for [GoLang](https://golang.org) applications. diff --git a/README.md b/README.md index 32c681e..8a07100 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Go Doc](_icons/godoc.svg)](https://pkg.go.dev/github.com/FishGoddess/cachego) [![License](_icons/license.svg)](https://www.apache.org/licenses/LICENSE-2.0.html) -[![License](_icons/coverage.svg)](_icons/coverage.svg) +[![Coverage](_icons/coverage.svg)](_icons/coverage.svg) ![Test](https://github.com/FishGoddess/cachego/actions/workflows/test.yml/badge.svg) **cachego** 是一个拥有分片机制的轻量级内存缓存库,API 友好,支持多种数据淘汰机制,可以应用于所有的 [GoLang](https://golang.org) 应用程序中。 diff --git a/_examples/task.go b/_examples/task.go index c9c27b0..db814ad 100644 --- a/_examples/task.go +++ b/_examples/task.go @@ -54,10 +54,5 @@ func main() { // Duration is the duration between two loop of fn, optional. // Run will start a new goroutine and run the task loop. // The task will stop if context is done. - task.New(printContextValue). - Before(beforePrint). - After(afterPrint). - Context(ctx). - Duration(time.Second). - Run() + task.New(printContextValue).Before(beforePrint).After(afterPrint).Context(ctx).Duration(time.Second).Run() } diff --git a/_icons/coverage.svg b/_icons/coverage.svg index be8453f..68ffdc7 100644 --- a/_icons/coverage.svg +++ b/_icons/coverage.svg @@ -10,7 +10,7 @@ coverage coverage - 98% - 98% + 97% + 97% \ No newline at end of file diff --git a/_icons/jetbrains.png b/_icons/jetbrains.png deleted file mode 100644 index 713d218..0000000 Binary files a/_icons/jetbrains.png and /dev/null differ diff --git a/cache.go b/cache.go index 4713cc7..e838fa4 100644 --- a/cache.go +++ b/cache.go @@ -16,7 +16,6 @@ package cachego import ( "context" - "sync" "time" "github.com/FishGoddess/cachego/pkg/task" @@ -27,20 +26,6 @@ const ( NoTTL = 0 ) -const ( - // standard cache is a simple cache with locked map. - // It evicts entries randomly if cache size reaches to max entries. - standard CacheType = "standard" - - // lru cache is a cache using lru to evict entries. - // More details see https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU). - lru CacheType = "lru" - - // lfu cache is a cache using lfu to evict entries. - // More details see https://en.wikipedia.org/wiki/Cache_replacement_policies#Least-frequently_used_(LFU). - lfu CacheType = "lfu" -) - var ( newCaches = map[CacheType]func(conf *config) Cache{ standard: newStandardCache, @@ -49,29 +34,6 @@ var ( } ) -// CacheType is the type of cache. -type CacheType string - -// String returns the cache type in string form. -func (ct CacheType) String() string { - return string(ct) -} - -// IsStandard returns if cache type is standard. -func (ct CacheType) IsStandard() bool { - return ct == standard -} - -// IsLRU returns if cache type is lru. -func (ct CacheType) IsLRU() bool { - return ct == lru -} - -// IsLFU returns if cache type is lfu. -func (ct CacheType) IsLFU() bool { - return ct == lfu -} - // Cache is the core interface of cachego. // We provide some implements including standard cache and sharding cache. type Cache interface { @@ -101,38 +63,10 @@ type Cache interface { // Reset resets cache to initial status which is like a new cache. Reset() - // Loader loads a value to cache. - // See Loader interface. - Loader -} - -type cache struct { - *config - Loader - - lock sync.RWMutex -} - -func (c *cache) setup(conf *config, cache Cache) { - c.config = conf - c.Loader = NewLoader(cache, conf.singleflight) -} - -// RunGCTask runs a gc task in a new goroutine and returns a cancel function to cancel the task. -// However, you don't need to call it manually for most time, instead, use options is a better choice. -// Making it a public function is for more customizations in some situations. -// For example, using options to run gc task is un-cancelable, so you can use it to run gc task by your own -// and get a cancel function to cancel the gc task. -func RunGCTask(cache Cache, duration time.Duration) (cancel func()) { - fn := func(ctx context.Context) { - cache.GC() - } - - ctx := context.Background() - ctx, cancel = context.WithCancel(ctx) - - go task.New(fn).Context(ctx).Duration(duration).Run() - return cancel + // Load loads a key with ttl to cache and returns an error if failed. + // We recommend you use this method to load missed keys to cache, + // because it may use singleflight to reduce the times calling load function. + Load(key string, ttl time.Duration, load func() (value interface{}, err error)) (value interface{}, err error) } func newCache(withReport bool, opts ...Option) (cache Cache, reporter *Reporter) { @@ -180,3 +114,20 @@ func NewCache(opts ...Option) (cache Cache) { func NewCacheWithReport(opts ...Option) (cache Cache, reporter *Reporter) { return newCache(true, opts...) } + +// RunGCTask runs a gc task in a new goroutine and returns a cancel function to cancel the task. +// However, you don't need to call it manually for most time, instead, use options is a better choice. +// Making it a public function is for more customizations in some situations. +// For example, using options to run gc task is un-cancelable, so you can use it to run gc task by your own +// and get a cancel function to cancel the gc task. +func RunGCTask(cache Cache, duration time.Duration) (cancel func()) { + fn := func(ctx context.Context) { + cache.GC() + } + + ctx := context.Background() + ctx, cancel = context.WithCancel(ctx) + + go task.New(fn).Context(ctx).Duration(duration).Run() + return cancel +} diff --git a/cache_test.go b/cache_test.go index b061db3..72413e7 100644 --- a/cache_test.go +++ b/cache_test.go @@ -25,35 +25,10 @@ const ( maxTestEntries = 10 ) -// go test -v -cover -run=^TestCacheType$ -func TestCacheType(t *testing.T) { - if standard.String() != string(standard) { - t.Fatalf("standard.String() %s is wrong", standard.String()) - } - - if lru.String() != string(lru) { - t.Fatalf("lru.String() %s is wrong", lru.String()) - } - - if lfu.String() != string(lfu) { - t.Fatalf("lfu.String() %s is wrong", lfu.String()) - } - - if !standard.IsStandard() { - t.Fatal("!standard.IsStandard()") - } - - if !lru.IsLRU() { - t.Fatal("!standard.IsLRU()") - } - - if !lfu.IsLFU() { - t.Fatal("!standard.IsLFU()") - } -} - type testCache struct { - cache + *config + loader *loader + count int32 } @@ -84,6 +59,10 @@ func (tc *testCache) GC() (cleans int) { func (tc *testCache) Reset() {} +func (tc *testCache) Load(key string, ttl time.Duration, load func() (value interface{}, err error)) (value interface{}, err error) { + return nil, nil +} + func testCacheGet(t *testing.T, cache Cache) { value, found := cache.Get("key") if found { @@ -273,7 +252,7 @@ func testCacheImplement(t *testing.T, cache Cache) { } } -// go test -v -cover=^TestNewCache$ +// go test -v -cover -count=1 -test.cpu=1=^TestNewCache$ func TestNewCache(t *testing.T) { cache := NewCache() @@ -317,7 +296,7 @@ func TestNewCache(t *testing.T) { cache = NewCache(WithLRU(0)) } -// go test -v -cover=^TestNewCacheWithReport$ +// go test -v -cover -count=1 -test.cpu=1=^TestNewCacheWithReport$ func TestNewCacheWithReport(t *testing.T) { cache, reporter := NewCacheWithReport() @@ -335,7 +314,7 @@ func TestNewCacheWithReport(t *testing.T) { } } -// go test -v -cover=^TestRunGCTask$ +// go test -v -cover -count=1 -test.cpu=1=^TestRunGCTask$ func TestRunGCTask(t *testing.T) { cache := new(testCache) diff --git a/cache_type.go b/cache_type.go new file mode 100644 index 0000000..95a65ea --- /dev/null +++ b/cache_type.go @@ -0,0 +1,52 @@ +// Copyright 2024 FishGoddess. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cachego + +const ( + // standard cache is a simple cache with locked map. + // It evicts entries randomly if cache size reaches to max entries. + standard CacheType = "standard" + + // lru cache is a cache using lru to evict entries. + // More details see https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU). + lru CacheType = "lru" + + // lfu cache is a cache using lfu to evict entries. + // More details see https://en.wikipedia.org/wiki/Cache_replacement_policies#Least-frequently_used_(LFU). + lfu CacheType = "lfu" +) + +// CacheType is the type of cache. +type CacheType string + +// String returns the cache type in string form. +func (ct CacheType) String() string { + return string(ct) +} + +// IsStandard returns if cache type is standard. +func (ct CacheType) IsStandard() bool { + return ct == standard +} + +// IsLRU returns if cache type is lru. +func (ct CacheType) IsLRU() bool { + return ct == lru +} + +// IsLFU returns if cache type is lfu. +func (ct CacheType) IsLFU() bool { + return ct == lfu +} diff --git a/cache_type_test.go b/cache_type_test.go new file mode 100644 index 0000000..fba7442 --- /dev/null +++ b/cache_type_test.go @@ -0,0 +1,44 @@ +// Copyright 2024 FishGoddess. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cachego + +import "testing" + +// go test -v -cover -count=1 -test.cpu=1 -run=^TestCacheType$ +func TestCacheType(t *testing.T) { + if standard.String() != string(standard) { + t.Fatalf("standard.String() %s is wrong", standard.String()) + } + + if lru.String() != string(lru) { + t.Fatalf("lru.String() %s is wrong", lru.String()) + } + + if lfu.String() != string(lfu) { + t.Fatalf("lfu.String() %s is wrong", lfu.String()) + } + + if !standard.IsStandard() { + t.Fatal("!standard.IsStandard()") + } + + if !lru.IsLRU() { + t.Fatal("!standard.IsLRU()") + } + + if !lfu.IsLFU() { + t.Fatal("!standard.IsLFU()") + } +} diff --git a/config_test.go b/config_test.go index 5758796..6491aa5 100644 --- a/config_test.go +++ b/config_test.go @@ -87,7 +87,7 @@ func isConfigEquals(conf1 *config, conf2 *config) bool { return true } -// go test -v -cover -run=^TestApplyOptions$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestApplyOptions$ func TestApplyOptions(t *testing.T) { got := &config{ shardings: 0, diff --git a/doc.go b/doc.go index 2fa3c4e..a9334bc 100644 --- a/doc.go +++ b/doc.go @@ -437,12 +437,7 @@ Package cachego provides an easy way to use foundation for your caching operatio // Duration is the duration between two loop of fn, optional. // Run will start a new goroutine and run the task loop. // The task will stop if context is done. - task.New(printContextValue). - Before(beforePrint). - After(afterPrint). - Context(ctx). - Duration(time.Second). - Run() + task.New(printContextValue).Before(beforePrint).After(afterPrint).Context(ctx).Duration(time.Second).Run() 10. clock: @@ -480,4 +475,4 @@ Package cachego provides an easy way to use foundation for your caching operatio package cachego // import "github.com/FishGoddess/cachego" // Version is the version string representation of cachego. -const Version = "v0.5.0" +const Version = "v0.6.0-alpha" diff --git a/entry_test.go b/entry_test.go index dbcec72..ed3e08d 100644 --- a/entry_test.go +++ b/entry_test.go @@ -24,7 +24,7 @@ const ( testDurationGap = 10 * time.Microsecond ) -// go test -v -cover -run=^TestNewEntry$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestNewEntry$ func TestNewEntry(t *testing.T) { e := newEntry("key", "value", 0, now) @@ -69,7 +69,7 @@ func TestNewEntry(t *testing.T) { } } -// go test -v -cover -run=^TestEntrySetup$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestEntrySetup$ func TestEntrySetup(t *testing.T) { e := newEntry("key", "value", 0, now) diff --git a/global_test.go b/global_test.go index 9e4ebad..9a113b3 100644 --- a/global_test.go +++ b/global_test.go @@ -29,7 +29,7 @@ func BenchmarkHash(b *testing.B) { } } -// go test -v -cover -run=^TestHash$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestHash$ func TestHash(t *testing.T) { hash := hash("test") if hash < 0 { @@ -37,7 +37,7 @@ func TestHash(t *testing.T) { } } -// go test -v -cover -run=^TestNow$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestNow$ func TestNow(t *testing.T) { got := now() expect := time.Now().UnixNano() @@ -47,7 +47,7 @@ func TestNow(t *testing.T) { } } -// go test -v -cover -run=^TestSetMapInitialCap$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestSetMapInitialCap$ func TestSetMapInitialCap(t *testing.T) { oldInitialCap := mapInitialCap @@ -67,7 +67,7 @@ func TestSetMapInitialCap(t *testing.T) { } } -// go test -v -cover -run=^TestSetSliceInitialCap$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestSetSliceInitialCap$ func TestSetSliceInitialCap(t *testing.T) { oldInitialCap := sliceInitialCap diff --git a/go.mod b/go.mod index ee81265..a1f8805 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/FishGoddess/cachego -go 1.17 +go 1.20 diff --git a/lfu.go b/lfu.go index 9727c7c..1705f4b 100644 --- a/lfu.go +++ b/lfu.go @@ -15,16 +15,20 @@ package cachego import ( + "sync" "time" "github.com/FishGoddess/cachego/pkg/heap" ) type lfuCache struct { - cache + *config itemMap map[string]*heap.Item itemHeap *heap.Heap + lock sync.RWMutex + + loader *loader } func newLFUCache(conf *config) Cache { @@ -33,11 +37,12 @@ func newLFUCache(conf *config) Cache { } cache := &lfuCache{ + config: conf, itemMap: make(map[string]*heap.Item, mapInitialCap), itemHeap: heap.New(sliceInitialCap), + loader: newLoader(conf.singleflight), } - cache.setup(conf, cache) return cache } @@ -137,7 +142,8 @@ func (lc *lfuCache) gc() (cleans int) { func (lc *lfuCache) reset() { lc.itemMap = make(map[string]*heap.Item, mapInitialCap) lc.itemHeap = heap.New(sliceInitialCap) - lc.Loader.Reset() + + lc.loader.Reset() } // Get gets the value of key from cache and returns value if found. @@ -193,3 +199,15 @@ func (lc *lfuCache) Reset() { lc.reset() } + +// Load loads a value by load function and sets it to cache. +// Returns an error if load failed. +func (lc *lfuCache) Load(key string, ttl time.Duration, load func() (value interface{}, err error)) (value interface{}, err error) { + value, err = lc.loader.Load(key, ttl, load) + if err != nil { + return value, err + } + + lc.Set(key, value, ttl) + return value, nil +} diff --git a/lfu_test.go b/lfu_test.go index 51a0de4..edaaf51 100644 --- a/lfu_test.go +++ b/lfu_test.go @@ -25,16 +25,17 @@ import ( func newTestLFUCache() *lfuCache { conf := newDefaultConfig() conf.maxEntries = maxTestEntries + return newLFUCache(conf).(*lfuCache) } -// go test -v -cover -run=^TestLFUCache$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestLFUCache$ func TestLFUCache(t *testing.T) { cache := newTestLFUCache() testCacheImplement(t, cache) } -// go test -v -cover -run=^TestLFUCacheEvict$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestLFUCacheEvict$ func TestLFUCacheEvict(t *testing.T) { cache := newTestLFUCache() @@ -54,6 +55,7 @@ func TestLFUCacheEvict(t *testing.T) { for i := cache.maxEntries*10 - cache.maxEntries; i < cache.maxEntries*10; i++ { for j := 0; j < i; j++ { data := strconv.Itoa(i) + cache.Set(data, data, time.Duration(i)*time.Second) cache.Get(data) } @@ -61,6 +63,7 @@ func TestLFUCacheEvict(t *testing.T) { for i := cache.maxEntries*10 - cache.maxEntries; i < cache.maxEntries*10; i++ { data := strconv.Itoa(i) + value, ok := cache.Get(data) if !ok || value.(string) != data { t.Fatalf("!ok %+v || value.(string) %s != data %s", !ok, value.(string), data) @@ -68,6 +71,7 @@ func TestLFUCacheEvict(t *testing.T) { } i := cache.maxEntries*10 - cache.maxEntries + for cache.itemHeap.Size() > 0 { item := cache.itemHeap.Pop() entry := item.Value.(*entry) @@ -81,7 +85,7 @@ func TestLFUCacheEvict(t *testing.T) { } } -// go test -v -cover -run=^TestLFUCacheEvictSimulate$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestLFUCacheEvictSimulate$ func TestLFUCacheEvictSimulate(t *testing.T) { cache := newTestLFUCache() diff --git a/load.go b/load.go index 054bfa3..b5e3f76 100644 --- a/load.go +++ b/load.go @@ -18,62 +18,37 @@ import ( "errors" "time" - "github.com/FishGoddess/cachego/pkg/singleflight" + flight "github.com/FishGoddess/cachego/pkg/singleflight" ) -// Loader loads a value to cache. -// All implements should store a cache inside in order to load value to cache. -type Loader interface { - // Load loads a key with ttl to cache and returns an error if failed. - // We recommend you use this method to load missed keys to cache because it may use singleflight to reduce the times calling load function. - Load(key string, ttl time.Duration, load func() (value interface{}, err error)) (value interface{}, err error) - - // Reset resets loader to initial status which is like a new loader. - Reset() -} - -// Loader loads a value to cache. -// We recommend you set enableSingleflight=true in NewLoader for reducing the times calling load function. +// loader loads values from somewhere. type loader struct { - cache Cache - group *singleflight.Group + group *flight.Group } -// NewLoader creates a loader with cache. -// It also creates a singleflight group to call load if enableSingleflight is true. -func NewLoader(cache Cache, enableSingleflight bool) Loader { - loader := &loader{ - cache: cache, - } +// newLoader creates a loader. +// It also creates a singleflight group to call load if singleflight is true. +func newLoader(singleflight bool) *loader { + loader := new(loader) - if enableSingleflight { - loader.group = singleflight.NewGroup(mapInitialCap) + if singleflight { + loader.group = flight.NewGroup(mapInitialCap) } return loader } -// Load loads a key with ttl to cache and returns an error if failed. +// Load loads a value of key with ttl and returns an error if failed. func (l *loader) Load(key string, ttl time.Duration, load func() (value interface{}, err error)) (value interface{}, err error) { if load == nil { return nil, errors.New("cachego: load function is nil in loader") } - if l.group != nil { - value, err = l.group.Call(key, load) - } else { - value, err = load() - } - - if err != nil { - return value, err - } - - if l.cache != nil { - l.cache.Set(key, value, ttl) + if l.group == nil { + return load() } - return value, err + return l.group.Call(key, load) } // Reset resets loader to initial status which is like a new loader. diff --git a/load_test.go b/load_test.go index 6275f01..0b473f9 100644 --- a/load_test.go +++ b/load_test.go @@ -26,13 +26,14 @@ type testLoadCache struct { value interface{} ttl time.Duration - loader Loader + loader *loader } func newTestLoadCache(singleflight bool) Cache { - cache := new(testLoadCache) - loader := NewLoader(cache, singleflight) - cache.loader = loader + cache := &testLoadCache{ + loader: newLoader(singleflight), + } + return cache } @@ -44,6 +45,7 @@ func (tlc *testLoadCache) Set(key string, value interface{}, ttl time.Duration) tlc.key = key tlc.value = value tlc.ttl = ttl + return nil } @@ -65,32 +67,20 @@ func (tlc *testLoadCache) Load(key string, ttl time.Duration, load func() (value return tlc.loader.Load(key, ttl, load) } -// go test -v -cover -run=^TestNewLoader$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestNewLoader$ func TestNewLoader(t *testing.T) { - l := NewLoader(nil, false) - - loader1, ok := l.(*loader) - if !ok { - t.Fatalf("l.(*loader) %T not ok", l) - } - - if loader1.group != nil { - t.Fatalf("loader1.group %+v != nil", loader1.group) - } - - l = NewLoader(nil, true) - - loader2, ok := l.(*loader) - if !ok { - t.Fatalf("l.(*loader) %T not ok", l) + loader := newLoader(false) + if loader.group != nil { + t.Fatalf("loader.group %+v != nil", loader.group) } - if loader2.group == nil { - t.Fatal("loader2.group == nil") + loader = newLoader(true) + if loader.group == nil { + t.Fatal("loader.group == nil") } } -// go test -v -cover -run=^TestLoaderLoad$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestLoaderLoad$ func TestLoaderLoad(t *testing.T) { cache := newTestLoadCache(false) loadCount := 0 @@ -119,7 +109,10 @@ func TestLoaderLoad(t *testing.T) { cache = newTestLoadCache(true) loadCount = 0 + var errs []error + var lock sync.Mutex var wg sync.WaitGroup + for i := int64(0); i < 100; i++ { wg.Add(1) @@ -131,16 +124,26 @@ func TestLoaderLoad(t *testing.T) { _, err := cache.Load("key", time.Duration(i), func() (value interface{}, err error) { time.Sleep(time.Second) loadCount++ + return str, nil }) if err != nil { - t.Fatal(err) + lock.Lock() + errs = append(errs, err) + lock.Unlock() } }(i) } wg.Wait() + + for _, err := range errs { + if err != nil { + t.Fatal(err) + } + } + if loadCount != 1 { t.Fatalf("loadCount %d != 1", loadCount) } diff --git a/lru.go b/lru.go index f2468ec..f023c77 100644 --- a/lru.go +++ b/lru.go @@ -16,14 +16,18 @@ package cachego import ( "container/list" + "sync" "time" ) type lruCache struct { - cache + *config elementMap map[string]*list.Element elementList *list.List + lock sync.RWMutex + + loader *loader } func newLRUCache(conf *config) Cache { @@ -32,11 +36,12 @@ func newLRUCache(conf *config) Cache { } cache := &lruCache{ + config: conf, elementMap: make(map[string]*list.Element, mapInitialCap), elementList: list.New(), + loader: newLoader(conf.singleflight), } - cache.setup(conf, cache) return cache } @@ -136,7 +141,8 @@ func (lc *lruCache) gc() (cleans int) { func (lc *lruCache) reset() { lc.elementMap = make(map[string]*list.Element, mapInitialCap) lc.elementList = list.New() - lc.Loader.Reset() + + lc.loader.Reset() } // Get gets the value of key from cache and returns value if found. @@ -192,3 +198,15 @@ func (lc *lruCache) Reset() { lc.reset() } + +// Load loads a value by load function and sets it to cache. +// Returns an error if load failed. +func (lc *lruCache) Load(key string, ttl time.Duration, load func() (value interface{}, err error)) (value interface{}, err error) { + value, err = lc.loader.Load(key, ttl, load) + if err != nil { + return value, err + } + + lc.Set(key, value, ttl) + return value, nil +} diff --git a/lru_test.go b/lru_test.go index aa9dd82..0060ddf 100644 --- a/lru_test.go +++ b/lru_test.go @@ -28,13 +28,13 @@ func newTestLRUCache() *lruCache { return newLRUCache(conf).(*lruCache) } -// go test -v -cover -run=^TestLRUCache$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestLRUCache$ func TestLRUCache(t *testing.T) { cache := newTestLRUCache() testCacheImplement(t, cache) } -// go test -v -cover -run=^TestLRUCacheEvict$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestLRUCacheEvict$ func TestLRUCacheEvict(t *testing.T) { cache := newTestLRUCache() @@ -74,7 +74,7 @@ func TestLRUCacheEvict(t *testing.T) { } } -// go test -v -cover -run=^TestLRUCacheEvictSimulate$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestLRUCacheEvictSimulate$ func TestLRUCacheEvictSimulate(t *testing.T) { cache := newTestLRUCache() diff --git a/option_test.go b/option_test.go index 5417b65..5a64b8e 100644 --- a/option_test.go +++ b/option_test.go @@ -19,7 +19,7 @@ import ( "time" ) -// go test -v -cover -run=^TestWithCacheName$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestWithCacheName$ func TestWithCacheName(t *testing.T) { got := &config{cacheName: ""} expect := &config{cacheName: "-"} @@ -30,7 +30,7 @@ func TestWithCacheName(t *testing.T) { } } -// go test -v -cover -run=^TestWithLRU$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestWithLRU$ func TestWithLRU(t *testing.T) { got := &config{cacheType: standard, maxEntries: 0} expect := &config{cacheType: lru, maxEntries: 666} @@ -41,7 +41,7 @@ func TestWithLRU(t *testing.T) { } } -// go test -v -cover -run=^TestWithLFU$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestWithLFU$ func TestWithLFU(t *testing.T) { got := &config{cacheType: standard, maxEntries: 0} expect := &config{cacheType: lfu, maxEntries: 999} @@ -52,7 +52,7 @@ func TestWithLFU(t *testing.T) { } } -// go test -v -cover -run=^TestWithShardings$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestWithShardings$ func TestWithShardings(t *testing.T) { got := &config{shardings: 0} expect := &config{shardings: 1024} @@ -63,7 +63,7 @@ func TestWithShardings(t *testing.T) { } } -// go test -v -cover -run=^TestWithDisableSingleflight$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestWithDisableSingleflight$ func TestWithDisableSingleflight(t *testing.T) { got := &config{singleflight: true} expect := &config{singleflight: false} @@ -74,7 +74,7 @@ func TestWithDisableSingleflight(t *testing.T) { } } -// go test -v -cover -run=^TestWithGC$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestWithGC$ func TestWithGC(t *testing.T) { got := &config{gcDuration: 0} expect := &config{gcDuration: 1024} @@ -85,7 +85,7 @@ func TestWithGC(t *testing.T) { } } -// go test -v -cover -run=^TestWithMaxScans$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestWithMaxScans$ func TestWithMaxScans(t *testing.T) { got := &config{maxScans: 0} expect := &config{maxScans: 1024} @@ -96,7 +96,7 @@ func TestWithMaxScans(t *testing.T) { } } -// go test -v -cover -run=^TestWithMaxEntries$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestWithMaxEntries$ func TestWithMaxEntries(t *testing.T) { got := &config{maxEntries: 0} expect := &config{maxEntries: 1024} @@ -107,7 +107,7 @@ func TestWithMaxEntries(t *testing.T) { } } -// go test -v -cover -run=^TestWithNow$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestWithNow$ func TestWithNow(t *testing.T) { now := func() int64 { return 0 @@ -122,7 +122,7 @@ func TestWithNow(t *testing.T) { } } -// go test -v -cover -run=^TestWithHash$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestWithHash$ func TestWithHash(t *testing.T) { hash := func(key string) int { return 0 @@ -137,7 +137,7 @@ func TestWithHash(t *testing.T) { } } -// go test -v -cover -run=^TestWithRecordMissed$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestWithRecordMissed$ func TestWithRecordMissed(t *testing.T) { got := &config{recordMissed: false} expect := &config{recordMissed: true} @@ -148,7 +148,7 @@ func TestWithRecordMissed(t *testing.T) { } } -// go test -v -cover -run=^TestWithRecordHit$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestWithRecordHit$ func TestWithRecordHit(t *testing.T) { got := &config{recordHit: false} expect := &config{recordHit: true} @@ -159,7 +159,7 @@ func TestWithRecordHit(t *testing.T) { } } -// go test -v -cover -run=^TestWithRecordGC$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestWithRecordGC$ func TestWithRecordGC(t *testing.T) { got := &config{recordGC: false} expect := &config{recordGC: true} @@ -170,7 +170,7 @@ func TestWithRecordGC(t *testing.T) { } } -// go test -v -cover -run=^TestWithRecordLoad$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestWithRecordLoad$ func TestWithRecordLoad(t *testing.T) { got := &config{recordLoad: false} expect := &config{recordLoad: true} @@ -181,7 +181,7 @@ func TestWithRecordLoad(t *testing.T) { } } -// go test -v -cover -run=^TestWithReportMissed$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestWithReportMissed$ func TestWithReportMissed(t *testing.T) { reportMissed := func(reporter *Reporter, key string) {} @@ -194,7 +194,7 @@ func TestWithReportMissed(t *testing.T) { } } -// go test -v -cover -run=^TestWithReportHit$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestWithReportHit$ func TestWithReportHit(t *testing.T) { reportHit := func(reporter *Reporter, key string, value interface{}) {} @@ -207,7 +207,7 @@ func TestWithReportHit(t *testing.T) { } } -// go test -v -cover -run=^TestWithReportGC$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestWithReportGC$ func TestWithReportGC(t *testing.T) { reportGC := func(reporter *Reporter, cost time.Duration, cleans int) {} @@ -220,7 +220,7 @@ func TestWithReportGC(t *testing.T) { } } -// go test -v -cover -run=^TestWithReportLoad$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestWithReportLoad$ func TestWithReportLoad(t *testing.T) { reportLoad := func(reporter *Reporter, key string, value interface{}, ttl time.Duration, err error) {} diff --git a/pkg/clock/clock_test.go b/pkg/clock/clock_test.go index f44b95e..aa70164 100644 --- a/pkg/clock/clock_test.go +++ b/pkg/clock/clock_test.go @@ -43,7 +43,7 @@ func BenchmarkClockNow(b *testing.B) { } } -// go test -v -cover -run=^TestNew$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestNew$ func TestNew(t *testing.T) { var clocks []*Clock @@ -58,7 +58,7 @@ func TestNew(t *testing.T) { } } -// go test -v -cover -run=^TestClock$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestClock$ func TestClock(t *testing.T) { testClock := New() diff --git a/pkg/heap/heap_test.go b/pkg/heap/heap_test.go index f496fcd..b74556b 100644 --- a/pkg/heap/heap_test.go +++ b/pkg/heap/heap_test.go @@ -33,7 +33,7 @@ func newTestData(count int) []int { return data } -// go test -v -cover -run=^TestItem$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestItem$ func TestItem(t *testing.T) { heap := New(64) @@ -107,7 +107,7 @@ func TestItem(t *testing.T) { } } -// go test -v -cover -run=^TestHeap$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestHeap$ func TestHeap(t *testing.T) { data := newTestData(10) t.Log(data) diff --git a/pkg/singleflight/singleflight_test.go b/pkg/singleflight/singleflight_test.go index 0367194..7daa777 100644 --- a/pkg/singleflight/singleflight_test.go +++ b/pkg/singleflight/singleflight_test.go @@ -55,13 +55,13 @@ func testGroupCall(t *testing.T, group *Group, concurrency int) { wg.Wait() } -// go test -v -cover -run=^TestGroupCall$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestGroupCall$ func TestGroupCall(t *testing.T) { group := NewGroup(128) testGroupCall(t, group, 100000) } -// go test -v -cover -run=^TestGroupCallMultiKey$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestGroupCallMultiKey$ func TestGroupCallMultiKey(t *testing.T) { group := NewGroup(128) @@ -78,7 +78,7 @@ func TestGroupCallMultiKey(t *testing.T) { wg.Wait() } -// go test -v -cover -run=^TestGroupDelete$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestGroupDelete$ func TestGroupDelete(t *testing.T) { group := NewGroup(128) @@ -114,7 +114,7 @@ func TestGroupDelete(t *testing.T) { } } -// go test -v -cover -run=^TestGroupReset$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestGroupReset$ func TestGroupReset(t *testing.T) { group := NewGroup(128) diff --git a/pkg/task/task_test.go b/pkg/task/task_test.go index c074c04..3b5fc3d 100644 --- a/pkg/task/task_test.go +++ b/pkg/task/task_test.go @@ -27,7 +27,7 @@ type testEntry struct { value string } -// go test -v -cover -run=^TestTickerTaskRun$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestTickerTaskRun$ func TestTickerTaskRun(t *testing.T) { before := testEntry{key: "before_key", value: "before_value"} fn := testEntry{key: "task_key", value: "task_value"} diff --git a/report_test.go b/report_test.go index 7f73ba7..9619ed0 100644 --- a/report_test.go +++ b/report_test.go @@ -39,13 +39,13 @@ func newTestReportableCache() (*reportableCache, *Reporter) { return cache.(*reportableCache), reporter } -// go test -v -cover -run=^TestReportableCache$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestReportableCache$ func TestReportableCache(t *testing.T) { cache, _ := newTestReportableCache() testCacheImplement(t, cache) } -// go test -v -cover -run=^TestReportableCacheReportMissed$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestReportableCacheReportMissed$ func TestReportableCacheReportMissed(t *testing.T) { cache, reporter := newTestReportableCache() cache.Set("key", 666, NoTTL) @@ -80,7 +80,7 @@ func TestReportableCacheReportMissed(t *testing.T) { } } -// go test -v -cover -run=^TestReportableCacheReportHit$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestReportableCacheReportHit$ func TestReportableCacheReportHit(t *testing.T) { cache, reporter := newTestReportableCache() cache.Set("key", 666, NoTTL) @@ -119,7 +119,7 @@ func TestReportableCacheReportHit(t *testing.T) { } } -// go test -v -cover -run=^TestReportableCacheReportGC$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestReportableCacheReportGC$ func TestReportableCacheReportGC(t *testing.T) { cache, reporter := newTestReportableCache() cache.Set("key1", 1, time.Millisecond) @@ -130,6 +130,7 @@ func TestReportableCacheReportGC(t *testing.T) { gcCount := uint64(0) checked := false + cache.reportGC = func(reporter *Reporter, cost time.Duration, cleans int) { if cost <= 0 { t.Fatalf("cost %d <= 0", cost) @@ -159,12 +160,13 @@ func TestReportableCacheReportGC(t *testing.T) { } } -// go test -v -cover -run=^TestReportableCacheReportLoad$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestReportableCacheReportLoad$ func TestReportableCacheReportLoad(t *testing.T) { cache, reporter := newTestReportableCache() loadCount := uint64(0) checked := false + cache.reportLoad = func(reporter *Reporter, key string, value interface{}, ttl time.Duration, err error) { if key != "load" { t.Fatalf("key %s is wrong", key) @@ -207,7 +209,7 @@ func TestReportableCacheReportLoad(t *testing.T) { } } -// go test -v -cover -run=^TestReporterCacheName$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestReporterCacheName$ func TestReporterCacheName(t *testing.T) { _, reporter := newTestReportableCache() if reporter.CacheName() != reporter.conf.cacheName { @@ -219,7 +221,7 @@ func TestReporterCacheName(t *testing.T) { } } -// go test -v -cover -run=^TestReporterCacheType$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestReporterCacheType$ func TestReporterCacheType(t *testing.T) { _, reporter := newTestReportableCache() if reporter.CacheType() != reporter.conf.cacheType { @@ -231,7 +233,7 @@ func TestReporterCacheType(t *testing.T) { } } -// go test -v -cover -run=^TestReporterCacheShardings$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestReporterCacheShardings$ func TestReporterCacheShardings(t *testing.T) { _, reporter := newTestReportableCache() if reporter.CacheShardings() != reporter.conf.shardings { @@ -243,7 +245,7 @@ func TestReporterCacheShardings(t *testing.T) { } } -// go test -v -cover -run=^TestReporterCacheGC$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestReporterCacheGC$ func TestReporterCacheGC(t *testing.T) { _, reporter := newTestReportableCache() if reporter.CacheGC() != reporter.conf.gcDuration { @@ -255,7 +257,7 @@ func TestReporterCacheGC(t *testing.T) { } } -// go test -v -cover -run=^TestReporterCacheSize$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestReporterCacheSize$ func TestReporterCacheSize(t *testing.T) { cache, reporter := newTestReportableCache() cache.Set("key1", 1, time.Millisecond) diff --git a/sharding.go b/sharding.go index 77f7fdd..096e5c3 100644 --- a/sharding.go +++ b/sharding.go @@ -38,15 +38,18 @@ func newShardingCache(conf *config, newCache func(conf *config) Cache) Cache { caches = append(caches, newCache(conf)) } - return &shardingCache{ + cache := &shardingCache{ config: conf, caches: caches, } + + return cache } func (sc *shardingCache) cacheOf(key string) Cache { hash := sc.hash(key) mask := len(sc.caches) - 1 + return sc.caches[hash&mask] } @@ -95,8 +98,8 @@ func (sc *shardingCache) Reset() { } } -// Load loads a key with ttl to cache and returns an error if failed. -// See Cache interface. +// Load loads a value by load function and sets it to cache. +// Returns an error if load failed. func (sc *shardingCache) Load(key string, ttl time.Duration, load func() (value interface{}, err error)) (value interface{}, err error) { return sc.cacheOf(key).Load(key, ttl, load) } diff --git a/sharding_test.go b/sharding_test.go index ad26ac0..71c7d22 100644 --- a/sharding_test.go +++ b/sharding_test.go @@ -26,18 +26,20 @@ const ( func newTestShardingCache() *shardingCache { conf := newDefaultConfig() conf.shardings = testShardings + return newShardingCache(conf, newStandardCache).(*shardingCache) } -// go test -v -cover -run=^TestShardingCache$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestShardingCache$ func TestShardingCache(t *testing.T) { cache := newTestShardingCache() testCacheImplement(t, cache) } -// go test -v -cover -run=^TestShardingCacheIndex$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestShardingCacheIndex$ func TestShardingCacheIndex(t *testing.T) { cache := newTestShardingCache() + if len(cache.caches) != testShardings { t.Fatalf("len(cache.caches) %d is wrong", len(cache.caches)) } diff --git a/standard.go b/standard.go index 080cdd0..83dd0ad 100644 --- a/standard.go +++ b/standard.go @@ -15,21 +15,26 @@ package cachego import ( + "sync" "time" ) type standardCache struct { - cache + *config entries map[string]*entry + lock sync.RWMutex + + loader *loader } func newStandardCache(conf *config) Cache { cache := &standardCache{ + config: conf, entries: make(map[string]*entry, mapInitialCap), + loader: newLoader(conf.singleflight), } - cache.setup(conf, cache) return cache } @@ -101,7 +106,7 @@ func (sc *standardCache) gc() (cleans int) { func (sc *standardCache) reset() { sc.entries = make(map[string]*entry, mapInitialCap) - sc.Loader.Reset() + sc.loader.Reset() } // Get gets the value of key from cache and returns value if found. @@ -157,3 +162,15 @@ func (sc *standardCache) Reset() { sc.reset() } + +// Load loads a value by load function and sets it to cache. +// Returns an error if load failed. +func (sc *standardCache) Load(key string, ttl time.Duration, load func() (value interface{}, err error)) (value interface{}, err error) { + value, err = sc.loader.Load(key, ttl, load) + if err != nil { + return value, err + } + + sc.Set(key, value, ttl) + return value, nil +} diff --git a/standard_test.go b/standard_test.go index d9bbfac..3af3b85 100644 --- a/standard_test.go +++ b/standard_test.go @@ -23,16 +23,17 @@ import ( func newTestStandardCache() *standardCache { conf := newDefaultConfig() conf.maxEntries = maxTestEntries + return newStandardCache(conf).(*standardCache) } -// go test -v -cover -run=^TestStandardCache$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestStandardCache$ func TestStandardCache(t *testing.T) { cache := newTestStandardCache() testCacheImplement(t, cache) } -// go test -v -cover -run=^TestStandardCacheEvict$ +// go test -v -cover -count=1 -test.cpu=1 -run=^TestStandardCacheEvict$ func TestStandardCacheEvict(t *testing.T) { cache := newTestStandardCache()