From 8b29ccee178854e5cc3a15de296472ffea020330 Mon Sep 17 00:00:00 2001 From: tiglog Date: Mon, 16 Oct 2023 20:49:23 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=20gcache?= =?UTF-8?q?=EF=BC=8C=E6=94=AF=E6=8C=81=20memory=E3=80=81redis=20=E5=92=8C?= =?UTF-8?q?=20filesystem=20=E4=B8=89=E7=A7=8D=20store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gcache/cache.go | 18 +++ gcache/cache_test.go | 51 ++++++++ gcache/config.go | 21 ---- gcache/fs_adapter.go | 214 ++++++++++++++++++++++++++++++++ gcache/hash.go | 42 ------- gcache/icache.go | 122 ------------------- gcache/memory.go | 255 --------------------------------------- gcache/memory_adapter.go | 155 ++++++++++++++++++++++++ gcache/memory_test.go | 31 ----- gcache/option.go | 68 ----------- gcache/redis.go | 10 -- gcache/redis_adapter.go | 87 +++++++++++++ gcache/types.go | 59 +++++++++ gcache/utils.go | 120 ++++++++++++++++++ 14 files changed, 704 insertions(+), 549 deletions(-) create mode 100644 gcache/cache.go create mode 100644 gcache/cache_test.go delete mode 100644 gcache/config.go create mode 100644 gcache/fs_adapter.go delete mode 100644 gcache/hash.go delete mode 100644 gcache/icache.go delete mode 100644 gcache/memory.go create mode 100644 gcache/memory_adapter.go delete mode 100644 gcache/memory_test.go delete mode 100644 gcache/option.go delete mode 100644 gcache/redis.go create mode 100644 gcache/redis_adapter.go create mode 100644 gcache/types.go create mode 100644 gcache/utils.go diff --git a/gcache/cache.go b/gcache/cache.go new file mode 100644 index 0000000..0627d23 --- /dev/null +++ b/gcache/cache.go @@ -0,0 +1,18 @@ +// +// cache.go +// Copyright (C) 2023 tiglog +// +// Distributed under terms of the MIT license. +// + +package gcache + +import "time" + +type ICache interface { + Set(key string, data []byte, expires time.Duration) error + Get(key string) ([]byte, error) + Delete(key string) error + Flush() error + Has(key string) bool +} diff --git a/gcache/cache_test.go b/gcache/cache_test.go new file mode 100644 index 0000000..b02a842 --- /dev/null +++ b/gcache/cache_test.go @@ -0,0 +1,51 @@ +// +// cache_test.go +// Copyright (C) 2023 tiglog +// +// Distributed under terms of the MIT license. +// + +package gcache_test + +import ( + "os" + "testing" + "time" + + "git.hexq.cn/tiglog/golib/gcache" + "git.hexq.cn/tiglog/golib/gtest" + "github.com/redis/go-redis/v9" +) + +var sample_name = "golib gcache" + +func TestRedisCache(t *testing.T) { + + REDIS_URL := os.Getenv("REDIS_URL") + // fmt.Println(REDIS_URL) + redisOpt, err := redis.ParseURL(REDIS_URL) + gtest.Nil(t, err) + var store gcache.ICache = gcache.NewRedisStore(gcache.RCClientOptions(redisOpt)) + + var sampleData = []byte(sample_name) + err = store.Set("name", sampleData, time.Minute*1) + gtest.Nil(t, err) + + r1, err := store.Get("name") + gtest.Nil(t, err) + gtest.Equal(t, string(r1), sample_name) +} + +func TestMemoryCache(t *testing.T) { + store := gcache.NewMemoryStore() + + var err error + var sampleData = []byte(sample_name) + err = store.Set("name", sampleData, time.Minute*1) + gtest.Nil(t, err) + + r1, err := store.Get("name") + gtest.Nil(t, err) + gtest.Equal(t, string(r1), sample_name) + +} diff --git a/gcache/config.go b/gcache/config.go deleted file mode 100644 index d4e482b..0000000 --- a/gcache/config.go +++ /dev/null @@ -1,21 +0,0 @@ -// -// config.go -// Copyright (C) 2023 tiglog -// -// Distributed under terms of the MIT license. -// - -package gcache - -import "time" - -type Config struct { - shards int - expiredCallback ExpiredCallback - hash IHash - clearInterval time.Duration -} - -func NewConfig() *Config { - return &Config{shards: 1024, hash: newDefaultHash(), clearInterval: 1 * time.Second} -} diff --git a/gcache/fs_adapter.go b/gcache/fs_adapter.go new file mode 100644 index 0000000..fa1fbf7 --- /dev/null +++ b/gcache/fs_adapter.go @@ -0,0 +1,214 @@ +// +// fs_adapter.go +// Copyright (C) 2023 tiglog +// +// Distributed under terms of the MIT license. +// + +package gcache + +import ( + "bytes" + "crypto/md5" + "encoding/hex" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "syscall" + "time" +) + +// Option is an optional type +type FCOption func(fs *FSStore) + +func BaseDirectory(base string) FCOption { + return func(fs *FSStore) { + fs.baseDir = base + } +} + +func FCSerializer(serializer Serializer) FCOption { + return func(fs *FSStore) { + fs.b = serializer + } +} + +func FCCacheKeyGenerator(fn KeyFunc) FCOption { + return func(fs *FSStore) { + fs.keyFn = fn + } +} + +const ( + defaultFilePerm os.FileMode = 0644 + defaultDirectoryFilePerm = 0755 +) + +func FilePathKeyFunc(s string) string { + + hashSum := md5.Sum([]byte(s)) + hashSumAsString := hex.EncodeToString(hashSum[:]) + + return filepath.Join(string(hashSumAsString[0:2]), + string(hashSumAsString[2:4]), + string(hashSumAsString[4:6]), hashSumAsString) +} + +func createDirectory(dir string) error { + return os.MkdirAll(dir, defaultDirectoryFilePerm) +} + +type FSStore struct { + baseDir string + b Serializer + keyFn KeyFunc +} + +func NewFSStore(baseDir string, opts ...FCOption) (*FSStore, error) { + + _, err := os.Stat(baseDir) + + if err != nil { //Directory does not exist..Let's create it + if err := createDirectory(baseDir); err != nil { + panic(fmt.Errorf("Base directory could not be created : %s", err)) + } + } + opts = append(opts, BaseDirectory(baseDir)) + + store := &FSStore{} + + for _, opt := range opts { + opt(store) + } + + if store.b == nil { + store.b = NewCacheSerializer() + } + + if len(strings.TrimSpace(store.baseDir)) == 0 { + return nil, errors.New("onecache : base directory not provided") + } + + if store.keyFn == nil { + store.keyFn = FilePathKeyFunc + } + + return store, nil +} + +func (fs *FSStore) Set(key string, data []byte, expiresAt time.Duration) error { + + path := fs.filePathFor(key) + + if err := createDirectory(filepath.Dir(path)); err != nil { + return err + } + + i := &Item{ExpiresAt: time.Now().Add(expiresAt), Data: data} + + b, err := fs.b.Serialize(i) + if err != nil { + return err + } + + return writeFile(path, b) +} + +func (fs *FSStore) Get(key string) ([]byte, error) { + + var b = new(bytes.Buffer) + + f, err := os.OpenFile(fs.filePathFor(key), os.O_RDONLY, 0644) + if err != nil { + pe, ok := err.(*os.PathError) + if !ok { + return nil, err + } + + if pe.Err == syscall.ENOENT && pe.Op == "open" { + return nil, ErrCacheMiss + } + + return nil, err + } + + if _, err := io.Copy(b, f); err != nil { + f.Close() + return nil, err + } + + f.Close() + + i := new(Item) + + if err := fs.b.DeSerialize(b.Bytes(), i); err != nil { + return nil, err + } + + if i.IsExpired() { + fs.Delete(key) + return nil, ErrCacheMiss + } + + return i.Data, nil +} + +func (fs *FSStore) Delete(key string) error { + return os.RemoveAll(fs.filePathFor(key)) +} + +func (fs *FSStore) Flush() error { + return os.RemoveAll(fs.baseDir) +} + +func (fs *FSStore) GC() { + + filepath.Walk( + fs.baseDir, + func(path string, finfo os.FileInfo, err error) error { + + if err != nil { + return err + } + + if finfo.IsDir() { + return nil + } + + currentItem := new(Item) + + byt, err := os.ReadFile(path) + + if err != nil { + return err + } + + if err = fs.b.DeSerialize(byt, currentItem); err != nil { + return err + } + + if currentItem.IsExpired() { + if err := os.Remove(path); !os.IsExist(err) { + return err + } + } + + return nil + }) +} + +func (fs *FSStore) Has(key string) bool { + _, err := os.Stat(fs.filePathFor(key)) + return !os.IsNotExist(err) +} + +func (fs *FSStore) filePathFor(key string) string { + return filepath.Join(fs.baseDir, fs.keyFn(key)) +} + +func writeFile(path string, b []byte) error { + return os.WriteFile(path, b, defaultFilePerm) +} diff --git a/gcache/hash.go b/gcache/hash.go deleted file mode 100644 index f45ab19..0000000 --- a/gcache/hash.go +++ /dev/null @@ -1,42 +0,0 @@ -// -// hash.go -// Copyright (C) 2023 tiglog -// -// Distributed under terms of the MIT license. -// - -package gcache - -// IHash is responsible for generating unsigned, 64-bit hash of provided string. IHash should minimize collisions -// (generating same hash for different strings) and while performance is also important fast functions are preferable (i.e. -// you can use FarmHash family). -type IHash interface { - Sum64(string) uint64 -} - -// newDefaultHash returns a new 64-bit FNV-1a IHash which makes no memory allocations. -// Its Sum64 method will lay the value out in big-endian byte order. -// See https://en.wikipedia.org/wiki/Fowler–Noll–Vo_hash_function -func newDefaultHash() IHash { - return fnv64a{} -} - -type fnv64a struct{} - -const ( - // offset64 FNVa offset basis. See https://en.wikipedia.org/wiki/Fowler–Noll–Vo_hash_function#FNV-1a_hash - offset64 = 14695981039346656037 - // prime64 FNVa prime value. See https://en.wikipedia.org/wiki/Fowler–Noll–Vo_hash_function#FNV-1a_hash - prime64 = 1099511628211 -) - -// Sum64 gets the string and returns its uint64 hash value. -func (f fnv64a) Sum64(key string) uint64 { - var hash uint64 = offset64 - for i := 0; i < len(key); i++ { - hash ^= uint64(key[i]) - hash *= prime64 - } - - return hash -} diff --git a/gcache/icache.go b/gcache/icache.go deleted file mode 100644 index 4a839f1..0000000 --- a/gcache/icache.go +++ /dev/null @@ -1,122 +0,0 @@ -// -// icache.go -// Copyright (C) 2023 tiglog -// -// Distributed under terms of the MIT license. -// - -package gcache - -import "time" - -// ExpiredCallback Callback the function when the key-value pair expires -// Note that it is executed after expiration -type ExpiredCallback func(k string, v interface{}) error - -type ICache interface { - //Set key to hold the string value. If key already holds a value, it is overwritten, regardless of its type. - //Any previous time to live associated with the key is discarded on successful SET operation. - //Example: - //c.Set("demo", 1) - //c.Set("demo", 1, WithEx(10*time.Second)) - //c.Set("demo", 1, WithEx(10*time.Second), WithNx()) - Set(k string, v interface{}, opts ...SetIOption) bool - //Get the value of key. - //If the key does not exist the special value nil,false is returned. - //Example: - //c.Get("demo") //nil, false - //c.Set("demo", "value") - //c.Get("demo") //"value", true - Get(k string) (interface{}, bool) - //GetSet Atomically sets key to value and returns the old value stored at key. - //Returns nil,false when key not exists. - //Example: - //c.GetSet("demo", 1) //nil,false - //c.GetSet("demo", 2) //1,true - GetSet(k string, v interface{}, opts ...SetIOption) (interface{}, bool) - //GetDel Get the value of key and delete the key. - //This command is similar to GET, except for the fact that it also deletes the key on success. - //Example: - //c.Set("demo", "value") - //c.GetDel("demo") //"value", true - //c.GetDel("demo") //nil, false - GetDel(k string) (interface{}, bool) - //Del Removes the specified keys. A key is ignored if it does not exist. - //Return the number of keys that were removed. - //Example: - //c.Set("demo1", "1") - //c.Set("demo2", "1") - //c.Del("demo1", "demo2", "demo3") //2 - Del(keys ...string) int - //DelExpired Only delete when key expires - //Example: - //c.Set("demo1", "1") - //c.Set("demo2", "1", WithEx(1*time.Second)) - //time.Sleep(1*time.Second) - //c.DelExpired("demo1", "demo2") //true - DelExpired(k string) bool - //Exists Returns if key exists. - //Return the number of exists keys. - //Example: - //c.Set("demo1", "1") - //c.Set("demo2", "1") - //c.Exists("demo1", "demo2", "demo3") //2 - Exists(keys ...string) bool - //Expire Set a timeout on key. - //After the timeout has expired, the key will automatically be deleted. - //Return false if the key not exist. - //Example: - //c.Expire("demo", 1*time.Second) // false - //c.Set("demo", "1") - //c.Expire("demo", 1*time.Second) // true - Expire(k string, d time.Duration) bool - //ExpireAt has the same effect and semantic as Expire, but instead of specifying the number of seconds representing the TTL (time to live), - //it takes an absolute Unix Time (seconds since January 1, 1970). A Time in the past will delete the key immediately. - //Return false if the key not exist. - //Example: - //c.ExpireAt("demo", time.Now().Add(10*time.Second)) // false - //c.Set("demo", "1") - //c.ExpireAt("demo", time.Now().Add(10*time.Second)) // true - ExpireAt(k string, t time.Time) bool - //Persist Remove the existing timeout on key. - //Return false if the key not exist. - //Example: - //c.Persist("demo") // false - //c.Set("demo", "1") - //c.Persist("demo") // true - Persist(k string) bool - //Ttl Returns the remaining time to live of a key that has a timeout. - //Returns 0,false if the key does not exist or if the key exist but has no associated expire. - //Example: - //c.Set("demo", "1") - //c.Ttl("demo") // 0,false - //c.Set("demo", "1", WithEx(10*time.Second)) - //c.Ttl("demo") // 10*time.Second,true - Ttl(k string) (time.Duration, bool) -} - -type IItem interface { - Expired() bool - CanExpire() bool - SetExpireAt(t time.Time) -} - -type Item struct { - v interface{} - expire time.Time -} - -func (i *Item) Expired() bool { - if !i.CanExpire() { - return false - } - return time.Now().After(i.expire) -} - -func (i *Item) CanExpire() bool { - return !i.expire.IsZero() -} - -func (i *Item) SetExpireAt(t time.Time) { - i.expire = t -} diff --git a/gcache/memory.go b/gcache/memory.go deleted file mode 100644 index 75f9b53..0000000 --- a/gcache/memory.go +++ /dev/null @@ -1,255 +0,0 @@ -// -// memory.go -// Copyright (C) 2023 tiglog -// -// Distributed under terms of the MIT license. -// - -package gcache - -import ( - "runtime" - "sync" - "time" -) - -func NewMemCache(opts ...ICacheOption) ICache { - conf := NewConfig() - for _, opt := range opts { - opt(conf) - } - - c := &memCache{ - shards: make([]*memCacheShard, conf.shards), - closed: make(chan struct{}), - shardMask: uint64(conf.shards - 1), - config: conf, - hash: conf.hash, - } - for i := 0; i < len(c.shards); i++ { - c.shards[i] = newMemCacheShard(conf) - } - if conf.clearInterval > 0 { - go func() { - ticker := time.NewTicker(conf.clearInterval) - defer ticker.Stop() - for { - select { - case <-ticker.C: - for _, shard := range c.shards { - shard.checkExpire() - } - case <-c.closed: - return - } - } - }() - } - cache := &MemCache{c} - // Associated finalizer function with obj. - // When the obj is unreachable, close the obj. - runtime.SetFinalizer(cache, func(cache *MemCache) { close(cache.closed) }) - return cache -} - -type MemCache struct { - *memCache -} - -type memCache struct { - shards []*memCacheShard - hash IHash - shardMask uint64 - config *Config - closed chan struct{} -} - -func (c *memCache) Set(k string, v interface{}, opts ...SetIOption) bool { - item := Item{v: v} - for _, opt := range opts { - if pass := opt(c, k, &item); !pass { - return false - } - } - hashedKey := c.hash.Sum64(k) - shard := c.getShard(hashedKey) - shard.set(k, &item) - return true -} - -func (c *memCache) Get(k string) (interface{}, bool) { - hashedKey := c.hash.Sum64(k) - shard := c.getShard(hashedKey) - return shard.get(k) -} - -func (c *memCache) GetSet(k string, v interface{}, opts ...SetIOption) (interface{}, bool) { - defer c.Set(k, v, opts...) - return c.Get(k) -} - -func (c *memCache) GetDel(k string) (interface{}, bool) { - defer c.Del(k) - return c.Get(k) -} - -func (c *memCache) Del(ks ...string) int { - var count int - for _, k := range ks { - hashedKey := c.hash.Sum64(k) - shard := c.getShard(hashedKey) - count += shard.del(k) - } - return count -} - -// DelExpired Only delete when key expires -func (c *memCache) DelExpired(k string) bool { - hashedKey := c.hash.Sum64(k) - shard := c.getShard(hashedKey) - return shard.delExpired(k) -} - -func (c *memCache) Exists(ks ...string) bool { - for _, k := range ks { - if _, found := c.Get(k); !found { - return false - } - } - return true -} - -func (c *memCache) Expire(k string, d time.Duration) bool { - v, found := c.Get(k) - if !found { - return false - } - return c.Set(k, v, WithEx(d)) -} - -func (c *memCache) ExpireAt(k string, t time.Time) bool { - v, found := c.Get(k) - if !found { - return false - } - return c.Set(k, v, WithExAt(t)) -} - -func (c *memCache) Persist(k string) bool { - v, found := c.Get(k) - if !found { - return false - } - return c.Set(k, v) -} - -func (c *memCache) Ttl(k string) (time.Duration, bool) { - hashedKey := c.hash.Sum64(k) - shard := c.getShard(hashedKey) - return shard.ttl(k) -} - -func (c *memCache) getShard(hashedKey uint64) (shard *memCacheShard) { - return c.shards[hashedKey&c.shardMask] -} - -////////////// -// shard // -////////////// - -type memCacheShard struct { - hashmap map[string]Item - lock sync.RWMutex - expiredCallback ExpiredCallback -} - -func newMemCacheShard(conf *Config) *memCacheShard { - return &memCacheShard{expiredCallback: conf.expiredCallback, hashmap: map[string]Item{}} -} - -func (c *memCacheShard) set(k string, item *Item) { - c.lock.Lock() - c.hashmap[k] = *item - c.lock.Unlock() - return -} - -func (c *memCacheShard) get(k string) (interface{}, bool) { - c.lock.RLock() - item, exist := c.hashmap[k] - c.lock.RUnlock() - if !exist { - return nil, false - } - if !item.Expired() { - return item.v, true - } - if c.delExpired(k) { - return nil, false - } - return c.get(k) -} - -func (c *memCacheShard) getSet(k string, item *Item) (interface{}, bool) { - defer c.set(k, item) - return c.get(k) -} - -func (c *memCacheShard) getDel(k string) (interface{}, bool) { - defer c.del(k) - return c.get(k) -} - -func (c *memCacheShard) del(k string) int { - var count int - c.lock.Lock() - v, found := c.hashmap[k] - if found { - delete(c.hashmap, k) - if !v.Expired() { - count++ - } - } - c.lock.Unlock() - return count -} - -// delExpired Only delete when key expires -func (c *memCacheShard) delExpired(k string) bool { - c.lock.Lock() - item, found := c.hashmap[k] - if !found || !item.Expired() { - c.lock.Unlock() - return false - } - delete(c.hashmap, k) - c.lock.Unlock() - if c.expiredCallback != nil { - _ = c.expiredCallback(k, item.v) - } - return true -} - -func (c *memCacheShard) ttl(k string) (time.Duration, bool) { - c.lock.RLock() - v, found := c.hashmap[k] - c.lock.RUnlock() - if !found || !v.CanExpire() || v.Expired() { - return 0, false - } - return v.expire.Sub(time.Now()), true -} - -func (c *memCacheShard) checkExpire() { - var expiredKeys []string - c.lock.RLock() - for k, item := range c.hashmap { - if item.Expired() { - expiredKeys = append(expiredKeys, k) - } - } - c.lock.RUnlock() - for _, k := range expiredKeys { - c.delExpired(k) - } -} diff --git a/gcache/memory_adapter.go b/gcache/memory_adapter.go new file mode 100644 index 0000000..d314837 --- /dev/null +++ b/gcache/memory_adapter.go @@ -0,0 +1,155 @@ +// +// memory_adapter.go +// Copyright (C) 2023 tiglog +// +// Distributed under terms of the MIT license. +// + +package gcache + +import ( + "sync" + "time" +) + +// Option defines options for creating a memory store +type MCOption func(i *InMemoryStore) + +// BufferSize configures the store to allow a maximum of n +func BufferSize(n int) MCOption { + return func(i *InMemoryStore) { + i.bufferSize = n + } +} + +// KeyFunc allows for dynamic generation of cache keys +func MCCacheKeyGenerator(fn KeyFunc) MCOption { + return func(i *InMemoryStore) { + i.keyfn = fn + } +} + +// New returns a configured in memory store +func NewMemoryStore(opts ...MCOption) *InMemoryStore { + i := &InMemoryStore{} + + for _, opt := range opts { + opt(i) + } + + if i.keyfn == nil { + i.keyfn = DefaultKeyFunc + } + + if i.data == nil { + if i.bufferSize == 0 { + i.bufferSize = 100 + } + + i.data = make(map[string]*Item, i.bufferSize) + } + + return i +} + +// Represents an in-memory store +type InMemoryStore struct { + lock sync.RWMutex + data map[string]*Item + + bufferSize int + keyfn KeyFunc +} + +func (i *InMemoryStore) Set(key string, data []byte, expires time.Duration) error { + i.lock.Lock() + + i.data[i.keyfn(key)] = &Item{ + ExpiresAt: time.Now().Add(expires), + Data: copyData(data), + } + + i.lock.Unlock() + return nil +} + +func (i *InMemoryStore) Get(key string) ([]byte, error) { + i.lock.RLock() + + item := i.data[i.keyfn(key)] + if item == nil { + i.lock.RUnlock() + return nil, ErrCacheMiss + } + + if item.IsExpired() { + i.lock.RUnlock() + i.Delete(key) + return nil, ErrCacheMiss + } + + i.lock.RUnlock() + return copyData(item.Data), nil +} + +func (i *InMemoryStore) Delete(key string) error { + i.lock.RLock() + + _, ok := i.data[i.keyfn(key)] + if !ok { + i.lock.RUnlock() + return ErrCacheMiss + } + + i.lock.RUnlock() + + i.lock.Lock() + delete(i.data, i.keyfn(key)) + i.lock.Unlock() + return nil +} + +func (i *InMemoryStore) Flush() error { + i.lock.Lock() + + i.data = make(map[string]*Item, i.bufferSize) + i.lock.Unlock() + return nil +} + +func (i *InMemoryStore) Has(key string) bool { + i.lock.RLock() + + _, ok := i.data[i.keyfn(key)] + i.lock.RUnlock() + return ok +} + +func (i *InMemoryStore) GC() { + i.lock.Lock() + + for k, item := range i.data { + if item.IsExpired() { + //No need to spawn a new goroutine since we + //still have the lock here + delete(i.data, k) + } + } + + i.lock.Unlock() +} + +func (i *InMemoryStore) count() int { + i.lock.Lock() + n := len(i.data) + i.lock.Unlock() + + return n +} + +func copyData(data []byte) []byte { + result := make([]byte, len(data)) + copy(result, data) + + return result +} diff --git a/gcache/memory_test.go b/gcache/memory_test.go deleted file mode 100644 index cdc5214..0000000 --- a/gcache/memory_test.go +++ /dev/null @@ -1,31 +0,0 @@ -// -// memory_test.go -// Copyright (C) 2023 tiglog -// -// Distributed under terms of the MIT license. -// - -package gcache_test - -import ( - "testing" - "time" - - "git.hexq.cn/tiglog/golib/gcache" - "git.hexq.cn/tiglog/golib/gtest" -) - -func TestUsage(t *testing.T) { - - c := gcache.NewMemCache() - c.Set("a", 1) - c.Set("b", 1, gcache.WithEx(1*time.Second)) - time.Sleep(1 * time.Second) - r1, ok := c.Get("a") // 1, true - gtest.True(t, ok) - gtest.Equal(t, 1, r1) - - r2, ok := c.Get("b") - gtest.False(t, ok) - gtest.Nil(t, r2) -} diff --git a/gcache/option.go b/gcache/option.go deleted file mode 100644 index 15e12d1..0000000 --- a/gcache/option.go +++ /dev/null @@ -1,68 +0,0 @@ -// -// option.go -// Copyright (C) 2023 tiglog -// -// Distributed under terms of the MIT license. -// - -package gcache - -import "time" - -// SetIOption The option used to cache set -type SetIOption func(ICache, string, IItem) bool - -// WithEx Set the specified expire time, in time.Duration. -func WithEx(d time.Duration) SetIOption { - return func(c ICache, k string, v IItem) bool { - v.SetExpireAt(time.Now().Add(d)) - return true - } -} - -// WithExAt Set the specified expire deadline, in time.Time. -func WithExAt(t time.Time) SetIOption { - return func(c ICache, k string, v IItem) bool { - v.SetExpireAt(t) - return true - } -} - -// ICacheOption The option used to create the cache object -type ICacheOption func(conf *Config) - -// WithShards set custom size of sharding. Default is 1024 -// The larger the size, the smaller the lock force, the higher the concurrency performance, -// and the higher the memory footprint, so try to choose a size that fits your business scenario -func WithShards(shards int) ICacheOption { - if shards <= 0 { - panic("Invalid shards") - } - return func(conf *Config) { - conf.shards = shards - } -} - -// WithExpiredCallback set custom expired callback function -// This callback function is called when the key-value pair expires -func WithExpiredCallback(ec ExpiredCallback) ICacheOption { - return func(conf *Config) { - conf.expiredCallback = ec - } -} - -// WithHash set custom hash key function -func WithHash(hash IHash) ICacheOption { - return func(conf *Config) { - conf.hash = hash - } -} - -// WithClearInterval set custom clear interval. -// Interval for clearing expired key-value pairs. The default value is 1 second -// If the d is 0, the periodic clearing function is disabled -func WithClearInterval(d time.Duration) ICacheOption { - return func(conf *Config) { - conf.clearInterval = d - } -} diff --git a/gcache/redis.go b/gcache/redis.go deleted file mode 100644 index a823503..0000000 --- a/gcache/redis.go +++ /dev/null @@ -1,10 +0,0 @@ -// -// redis.go -// Copyright (C) 2023 tiglog -// -// Distributed under terms of the MIT license. -// - -package gcache - -// TODO 暂时不需要,后面再实现 diff --git a/gcache/redis_adapter.go b/gcache/redis_adapter.go new file mode 100644 index 0000000..09861bc --- /dev/null +++ b/gcache/redis_adapter.go @@ -0,0 +1,87 @@ +// +// redis_adapter.go +// Copyright (C) 2023 tiglog +// +// Distributed under terms of the MIT license. +// + +package gcache + +import ( + "context" + "time" + + "github.com/redis/go-redis/v9" +) + +// Option is a redis option type +type RCOption func(r *RedisStore) + +// ClientOptions is an Option type that allows configuring a redis client +func RCClientOptions(opts *redis.Options) RCOption { + return func(r *RedisStore) { + r.client = redis.NewClient(opts) + } +} + +// CacheKeyGenerator allows configuring the cache key generation process +func RCCacheKeyGenerator(fn KeyFunc) RCOption { + return func(r *RedisStore) { + r.keyFn = fn + } +} + +type RedisStore struct { + client *redis.Client + + keyFn KeyFunc +} + +// New returns a new RedisStore by applying all options passed into it +// It also sets sensible defaults too +func NewRedisStore(opts ...RCOption) *RedisStore { + r := &RedisStore{} + + for _, opt := range opts { + opt(r) + } + + if r.client == nil { + panic("需要指定 redis 的连接参数") + } + + if r.keyFn == nil { + r.keyFn = DefaultKeyFunc + } + + return r +} + +func (r *RedisStore) Set(k string, data []byte, expires time.Duration) error { + cmd := r.client.Set(context.Background(), r.key(k), data, expires) + return cmd.Err() +} + +func (r *RedisStore) Get(key string) ([]byte, error) { + return r.client.Get(context.Background(), r.key(key)).Bytes() +} + +func (r *RedisStore) Delete(key string) error { + return r.client.Del(context.Background(), r.key(key)).Err() +} + +func (r *RedisStore) Flush() error { + return r.client.FlushDB(context.Background()).Err() +} + +func (r *RedisStore) Has(key string) bool { + if _, err := r.Get(key); err != nil { + return false + } + + return true +} + +func (r *RedisStore) key(k string) string { + return r.keyFn(k) +} diff --git a/gcache/types.go b/gcache/types.go new file mode 100644 index 0000000..1e6c00c --- /dev/null +++ b/gcache/types.go @@ -0,0 +1,59 @@ +// +// tyeps.go +// Copyright (C) 2023 tiglog +// +// Distributed under terms of the MIT license. +// + +package gcache + +import ( + "errors" + "time" +) + +const ( + EXPIRES_DEFAULT = time.Duration(0) + EXPIRES_FOREVER = time.Duration(-1) +) + +var ( + ErrCacheMiss = errors.New("Key not found") + ErrCacheNotStored = errors.New("Data not stored") + ErrCacheNotSupported = errors.New("Operation not supported") + ErrCacheDataCannotBeIncreasedOrDecreased = errors.New(` + Data isn't an integer/string type. Hence, it cannot be increased or decreased`) +) + +// DefaultKeyFunc is the default implementation of cache keys +// All it does is to preprend "onecache:" to the key sent in by client code +func DefaultKeyFunc(s string) string { + return "gcache:" + s +} + +// Item identifes a cached piece of data +type Item struct { + ExpiresAt time.Time + Data []byte +} + +// Interface for all onecache store implementations +type Store interface { + Set(key string, data []byte, expires time.Duration) error + Get(key string) ([]byte, error) + Delete(key string) error + Flush() error + Has(key string) bool +} + +// Some stores like redis and memcache automatically clear out the cache +// But for the filesystem and in memory, this cannot be said. +// Stores that have to manually clear out the cached data should implement this method. +// It's implementation should re run this function everytime the interval is reached +// Say every 5 minutes. +type GarbageCollector interface { + GC() +} + +// KeyFunc defines a transformer for cache keys +type KeyFunc func(s string) string diff --git a/gcache/utils.go b/gcache/utils.go new file mode 100644 index 0000000..07072f7 --- /dev/null +++ b/gcache/utils.go @@ -0,0 +1,120 @@ +// +// utils.go +// Copyright (C) 2023 tiglog +// +// Distributed under terms of the MIT license. +// + +package gcache + +import ( + "bytes" + "encoding/gob" + "strconv" + "time" +) + +// Helper method to check if an item is expired. +// Current usecase for this is for garbage collection +func (i *Item) IsExpired() bool { + return time.Now().After(i.ExpiresAt) +} + +type Serializer interface { + Serialize(i interface{}) ([]byte, error) + DeSerialize(data []byte, i interface{}) error +} + +func NewCacheSerializer() *CacheSerializer { + return &CacheSerializer{} +} + +// Helper to serialize and deserialize types +type CacheSerializer struct { +} + +// Convert a given type into a byte array +// Caveat -> Types you create might have to be registered with the encoding/gob package +func (b *CacheSerializer) Serialize(i interface{}) ([]byte, error) { + + if b, ok := i.([]byte); ok { + return b, nil + } + + var buf bytes.Buffer + + enc := gob.NewEncoder(&buf) + + if err := enc.Encode(i); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +// Writes a byte array into a type. +func (b *CacheSerializer) DeSerialize(data []byte, i interface{}) error { + + if b, ok := i.(*[]byte); ok { + *b = data + return nil + } + + return gob.NewDecoder(bytes.NewBuffer(data)).Decode(i) +} + +// Increment increases the value of an item by the specified number of steps +func Increment(val interface{}, steps int) (interface{}, error) { + + var ret interface{} + + switch val.(type) { + + case int: + ret = val.(int) + steps + + case int32: + ret = val.(int32) + int32(steps) + + case int64: + ret = val.(int64) + int64(steps) + + case uint: + ret = val.(uint) + uint(steps) + + case uint8: + ret = val.(uint8) + uint8(steps) + + case uint16: + ret = val.(uint16) + uint16(steps) + + case uint32: + ret = val.(uint32) + uint32(steps) + + case uint64: + ret = val.(uint64) + uint64(steps) + + case string: + + num, err := strconv.Atoi(val.(string)) + + if err != nil { + return -0, err + } + + num += steps + + ret = strconv.Itoa(num) + + default: + return -0, ErrCacheDataCannotBeIncreasedOrDecreased + } + + return ret, nil + +} + +// Decrement decreases the value of an item by the specified number of steps +func Decrement(val interface{}, steps int) (interface{}, error) { + return Increment(val, steps*-1) +}