diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cec6c49..c4499ccb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Note: now we force the web admin service serving HTTP only. - [Fix 5022: Miss assigning listener to graceful Server](https://github.com/beego/beego/pull/5028) - [Fix 4955: Make commands and Docker compose for ORM unit tests](https://github.com/beego/beego/pull/5031) - [add read through for cache module](https://github.com/beego/beego/pull/5116) +- [add singleflight cache for cache module](https://github.com/beego/beego/pull/5119) # v2.0.4 diff --git a/client/cache/error_code.go b/client/cache/error_code.go index 1d130249..39549a55 100644 --- a/client/cache/error_code.go +++ b/client/cache/error_code.go @@ -123,6 +123,15 @@ var InvalidSsdbCacheValue = berror.DefineCode(4002022, moduleName, "InvalidSsdbC SSDB cache only accept string value. Please check your input. `) +var InvalidLoadFunc = berror.DefineCode(4002023, moduleName, "InvalidLoadFunc", ` +Invalid load function for read-through pattern decorator. +You should pass a valid(non-nil) load function when initiate the decorator instance. +`) + +var LoadFuncFailed = berror.DefineCode(4002024, moduleName, "LoadFuncFailed", ` +Failed to load data, please check whether the loadfunc is correct +`) + var InvalidInitParameters = berror.DefineCode(4002025, moduleName, "InvalidInitParameters", ` Invalid init cache parameters. You can check the related function to confirm that if you pass correct parameters or configure to initiate a Cache instance. @@ -133,15 +142,6 @@ Failed to execute the StoreFunc. Please check the log to make sure the StoreFunc works for the specific key and value. `) -var InvalidLoadFunc = berror.DefineCode(4002023, moduleName, "InvalidLoadFunc", ` -Invalid load function for read-through pattern decorator. -You should pass a valid(non-nil) load function when initiate the decorator instance. -`) - -var LoadFuncFailed = berror.DefineCode(4002024, moduleName, "InvalidLoadFunc", ` -Failed to load data, please check whether the loadfunc is correct -`) - var DeleteFileCacheItemFailed = berror.DefineCode(5002001, moduleName, "DeleteFileCacheItemFailed", ` Beego try to delete file cache item failed. Please check whether Beego generated file correctly. diff --git a/client/cache/singleflight.go b/client/cache/singleflight.go new file mode 100644 index 00000000..1d42f28e --- /dev/null +++ b/client/cache/singleflight.go @@ -0,0 +1,63 @@ +// Copyright 2014 beego Author. 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 cache + +import ( + "context" + "time" + + "github.com/beego/beego/v2/core/berror" + "golang.org/x/sync/singleflight" +) + +// SingleflightCache +// This is a very simple decorator mode +type SingleflightCache struct { + Cache + group *singleflight.Group + expiration time.Duration + loadFunc func(ctx context.Context, key string) (any, error) +} + +// NewSingleflightCache create SingleflightCache +func NewSingleflightCache(c Cache, expiration time.Duration, + loadFunc func(ctx context.Context, key string) (any, error), +) (Cache, error) { + if loadFunc == nil { + return nil, berror.Error(InvalidLoadFunc, "loadFunc cannot be nil") + } + return &SingleflightCache{ + Cache: c, + group: &singleflight.Group{}, + expiration: expiration, + loadFunc: loadFunc, + }, nil +} + +// Get In the Get method, single flight is used to load data and write back the cache. +func (s *SingleflightCache) Get(ctx context.Context, key string) (any, error) { + val, err := s.Cache.Get(ctx, key) + if val == nil || err != nil { + val, err, _ = s.group.Do(key, func() (interface{}, error) { + v, er := s.loadFunc(ctx, key) + if er != nil { + return nil, berror.Wrap(er, LoadFuncFailed, "cache unable to load data") + } + er = s.Cache.Put(ctx, key, v, s.expiration) + return v, er + }) + } + return val, err +} diff --git a/client/cache/singleflight_test.go b/client/cache/singleflight_test.go new file mode 100644 index 00000000..af691767 --- /dev/null +++ b/client/cache/singleflight_test.go @@ -0,0 +1,72 @@ +// Copyright 2014 beego Author. 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 cache + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestSingleflight_Memory_Get(t *testing.T) { + bm, err := NewCache("memory", `{"interval":20}`) + assert.Nil(t, err) + + testSingleflightCacheConcurrencyGet(t, bm) +} + +func TestSingleflight_file_Get(t *testing.T) { + fc := NewFileCache().(*FileCache) + fc.CachePath = "////aaa" + err := fc.Init() + assert.NotNil(t, err) + fc.CachePath = getTestCacheFilePath() + err = fc.Init() + assert.Nil(t, err) + + testSingleflightCacheConcurrencyGet(t, fc) +} + +func testSingleflightCacheConcurrencyGet(t *testing.T, bm Cache) { + key, value := "key3", "value3" + db := &MockOrm{keysMap: map[string]int{key: 1}, kvs: map[string]any{key: value}} + c, err := NewSingleflightCache(bm, 10*time.Second, + func(ctx context.Context, key string) (any, error) { + val, er := db.Load(key) + if er != nil { + return nil, er + } + return val, nil + }) + assert.Nil(t, err) + + var wg sync.WaitGroup + wg.Add(10) + for i := 0; i < 10; i++ { + go func() { + defer wg.Done() + val, err := c.Get(context.Background(), key) + if err != nil { + t.Error(err) + } + assert.Equal(t, value, val) + }() + time.Sleep(1 * time.Millisecond) + } + wg.Wait() +} diff --git a/client/cache/write_through_test.go b/client/cache/write_through_test.go index a25c74a5..9f2224d6 100644 --- a/client/cache/write_through_test.go +++ b/client/cache/write_through_test.go @@ -27,7 +27,7 @@ import ( ) func TestWriteThoughCache_Set(t *testing.T) { - var mockDbStore = make(map[string]any) + mockDbStore := make(map[string]any) testCases := []struct { name string diff --git a/go.mod b/go.mod index 4624819c..200147fd 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,7 @@ require ( go.opentelemetry.io/otel/sdk v1.8.0 go.opentelemetry.io/otel/trace v1.8.0 golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd + golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f google.golang.org/grpc v1.40.0 google.golang.org/protobuf v1.28.1 gopkg.in/yaml.v2 v2.4.0 diff --git a/go.sum b/go.sum index bb43d703..7088a849 100644 --- a/go.sum +++ b/go.sum @@ -446,6 +446,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=