diff --git a/client/cache/write_delete.go b/client/cache/write_delete.go new file mode 100644 index 00000000..d3572138 --- /dev/null +++ b/client/cache/write_delete.go @@ -0,0 +1,49 @@ +// 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" + "errors" + "fmt" + "github.com/beego/beego/v2/core/berror" +) + +type WriteDeleteCache struct { + Cache + storeFunc func(ctx context.Context, key string, val any) error +} + +// NewWriteDeleteCache creates a write delete cache pattern decorator. +// The fn is the function that persistent the key and val. +func NewWriteDeleteCache(cache Cache, fn func(ctx context.Context, key string, val any) error) (*WriteDeleteCache, error) { + if fn == nil || cache == nil { + return nil, berror.Error(InvalidInitParameters, "cache or storeFunc can not be nil") + } + + w := &WriteDeleteCache{ + Cache: cache, + storeFunc: fn, + } + return w, nil +} + +func (w *WriteDeleteCache) Set(ctx context.Context, key string, val any) error { + err := w.storeFunc(ctx, key, val) + if err != nil && !errors.Is(err, context.DeadlineExceeded) { + return berror.Wrap(err, PersistCacheFailed, fmt.Sprintf("key: %s, val: %v", key, val)) + } + return w.Cache.Delete(ctx, key) +} diff --git a/client/cache/write_delete_test.go b/client/cache/write_delete_test.go new file mode 100644 index 00000000..72834276 --- /dev/null +++ b/client/cache/write_delete_test.go @@ -0,0 +1,203 @@ +// 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" + "github.com/stretchr/testify/assert" + "testing" + "time" + + "github.com/beego/beego/v2/core/berror" +) + +func TestWriteDeleteCache_Set(t *testing.T) { + mockDbStore := make(map[string]any) + + cancels := make([]func(), 0) + defer func() { + for _, cancel := range cancels { + cancel() + } + }() + + testCases := []struct { + name string + cache Cache + storeFunc func(ctx context.Context, key string, val any) error + ctx context.Context + key string + value any + wantErr error + before func(Cache) + after func() + }{ + { + name: "store key/value in db fail", + cache: NewMemoryCache(), + storeFunc: func(ctx context.Context, key string, val any) error { + return errors.New("failed") + }, + ctx: context.TODO(), + wantErr: berror.Wrap(errors.New("failed"), PersistCacheFailed, + fmt.Sprintf("key: %s, val: %v", "", nil)), + before: func(cache Cache) {}, + after: func() {}, + }, + { + name: "store key/value success", + cache: NewMemoryCache(), + storeFunc: func(ctx context.Context, key string, val any) error { + mockDbStore[key] = val + return nil + }, + ctx: context.TODO(), + key: "hello", + value: "world", + before: func(cache Cache) { + _ = cache.Put(context.Background(), "hello", "testVal", 10*time.Second) + }, + after: func() { + delete(mockDbStore, "hello") + }, + }, + { + name: "store key/value timeout", + cache: NewMemoryCache(), + storeFunc: func(ctx context.Context, key string, val any) error { + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(3 * time.Second): + mockDbStore[key] = val + return nil + } + + }, + ctx: func() context.Context { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + cancels = append(cancels, cancel) + return ctx + + }(), + key: "hello", + value: nil, + before: func(cache Cache) { + _ = cache.Put(context.Background(), "hello", "testVal", 10*time.Second) + }, + after: func() {}, + }, + } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + w, err := NewWriteDeleteCache(tt.cache, tt.storeFunc) + if err != nil { + assert.EqualError(t, tt.wantErr, err.Error()) + return + } + + tt.before(tt.cache) + defer func() { + tt.after() + }() + + err = w.Set(tt.ctx, tt.key, tt.value) + if err != nil { + assert.EqualError(t, tt.wantErr, err.Error()) + return + } + + _, err = w.Get(tt.ctx, tt.key) + assert.Equal(t, ErrKeyNotExist, err) + + vv, _ := mockDbStore[tt.key] + assert.Equal(t, tt.value, vv) + }) + } +} + +func TestNewWriteDeleteCache(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 *WriteDeleteCache + 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: &WriteDeleteCache{ + Cache: underlyingCache, + storeFunc: storeFunc, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := NewWriteDeleteCache(tt.args.cache, tt.args.fn) + assert.Equal(t, tt.wantErr, err) + if err != nil { + return + } + }) + } +} + +func ExampleNewWriteDeleteCache() { + c := NewMemoryCache() + wtc, err := NewWriteDeleteCache(c, func(ctx context.Context, key string, val any) error { + fmt.Printf("write data to somewhere key %s, val %v \n", key, val) + return nil + }) + if err != nil { + panic(err) + } + err = wtc.Set(context.Background(), + "/biz/user/id=1", "I am user 1") + if err != nil { + panic(err) + } + // Output: + // write data to somewhere key /biz/user/id=1, val I am user 1 +}