From badab8eafdf5dfb9da08ec4bcad6c7bc2fe1919f Mon Sep 17 00:00:00 2001 From: hookokoko Date: Sat, 17 Dec 2022 10:25:39 +0800 Subject: [PATCH] feature: add write though for cache mode (#5117) * feature: add writethough for cache mode --- CHANGELOG.md | 1 + client/cache/error_code.go | 10 +++ client/cache/write_through.go | 48 ++++++++++ client/cache/write_through_test.go | 136 +++++++++++++++++++++++++++++ 4 files changed, 195 insertions(+) create mode 100644 client/cache/write_through.go create mode 100644 client/cache/write_through_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 54ef261e..0cec6c49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ # developing +- [Fix 5117: support write though cache](https://github.com/beego/beego/pull/5117) # v2.0.7 - [Upgrade github.com/go-kit/kit, CVE-2022-24450](https://github.com/beego/beego/pull/5121) diff --git a/client/cache/error_code.go b/client/cache/error_code.go index 55b0f21b..1d130249 100644 --- a/client/cache/error_code.go +++ b/client/cache/error_code.go @@ -123,6 +123,16 @@ var InvalidSsdbCacheValue = berror.DefineCode(4002022, moduleName, "InvalidSsdbC SSDB cache only accept string value. Please check your input. `) +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. +`) + +var PersistCacheFailed = berror.DefineCode(4002026, moduleName, "PersistCacheFailed", ` +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. diff --git a/client/cache/write_through.go b/client/cache/write_through.go new file mode 100644 index 00000000..10334b2e --- /dev/null +++ b/client/cache/write_through.go @@ -0,0 +1,48 @@ +// 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" + "fmt" + "time" + + "github.com/beego/beego/v2/core/berror" +) + +type WriteThoughCache struct { + Cache + storeFunc func(ctx context.Context, key string, val any) error +} + +func NewWriteThoughCache(cache Cache, fn func(ctx context.Context, key string, val any) error) (*WriteThoughCache, error) { + if fn == nil || cache == nil { + return nil, berror.Error(InvalidInitParameters, "cache or storeFunc can not be nil") + } + + w := &WriteThoughCache{ + Cache: cache, + storeFunc: fn, + } + return w, nil +} + +func (w *WriteThoughCache) Set(ctx context.Context, key string, val any, expiration time.Duration) error { + err := w.storeFunc(ctx, key, val) + if err != nil { + return berror.Wrap(err, PersistCacheFailed, fmt.Sprintf("key: %s, val: %v", key, val)) + } + return w.Cache.Put(ctx, key, val, expiration) +} diff --git a/client/cache/write_through_test.go b/client/cache/write_through_test.go new file mode 100644 index 00000000..a25c74a5 --- /dev/null +++ b/client/cache/write_through_test.go @@ -0,0 +1,136 @@ +// 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. + +// nolint +package cache + +import ( + "context" + "errors" + "fmt" + "testing" + "time" + + "github.com/beego/beego/v2/core/berror" + "github.com/stretchr/testify/assert" +) + +func TestWriteThoughCache_Set(t *testing.T) { + var mockDbStore = make(map[string]any) + + testCases := []struct { + name string + cache Cache + storeFunc func(ctx context.Context, key string, val any) error + key string + value any + wantErr error + }{ + { + name: "store key/value in db fail", + cache: NewMemoryCache(), + storeFunc: func(ctx context.Context, key string, val any) error { + return errors.New("failed") + }, + wantErr: berror.Wrap(errors.New("failed"), PersistCacheFailed, + fmt.Sprintf("key: %s, val: %v", "", nil)), + }, + { + name: "store key/value success", + cache: NewMemoryCache(), + storeFunc: func(ctx context.Context, key string, val any) error { + mockDbStore[key] = val + return nil + }, + key: "hello", + value: "world", + }, + } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + w, err := NewWriteThoughCache(tt.cache, tt.storeFunc) + if err != nil { + assert.EqualError(t, tt.wantErr, err.Error()) + return + } + + err = w.Set(context.Background(), tt.key, tt.value, 60*time.Second) + if err != nil { + assert.EqualError(t, tt.wantErr, err.Error()) + return + } + + val, err := w.Get(context.Background(), tt.key) + assert.Nil(t, err) + assert.Equal(t, tt.value, val) + + vv, ok := mockDbStore[tt.key] + assert.True(t, ok) + assert.Equal(t, tt.value, vv) + }) + } +} + +func TestNewWriteThoughCache(t *testing.T) { + underlyingCache := NewMemoryCache() + storeFunc := func(ctx context.Context, key string, val any) error { return nil } + + type args struct { + cache Cache + fn func(ctx context.Context, key string, val any) error + } + tests := []struct { + name string + args args + wantRes *WriteThoughCache + wantErr error + }{ + { + name: "nil cache parameters", + args: args{ + cache: nil, + fn: storeFunc, + }, + wantErr: berror.Error(InvalidInitParameters, "cache or storeFunc can not be nil"), + }, + { + name: "nil storeFunc parameters", + args: args{ + cache: underlyingCache, + fn: nil, + }, + wantErr: berror.Error(InvalidInitParameters, "cache or storeFunc can not be nil"), + }, + { + name: "init write-though cache success", + args: args{ + cache: underlyingCache, + fn: storeFunc, + }, + wantRes: &WriteThoughCache{ + Cache: underlyingCache, + storeFunc: storeFunc, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := NewWriteThoughCache(tt.args.cache, tt.args.fn) + assert.Equal(t, tt.wantErr, err) + if err != nil { + return + } + }) + } +}