From 5a07cd3f4edefcda4cde5a43618822aa537b2526 Mon Sep 17 00:00:00 2001 From: Francesco Cosentino Date: Sun, 15 Jan 2023 17:59:59 +0100 Subject: [PATCH 1/5] Redis backend: first implementation --- backend/options.go | 13 ++- backend/redis.go | 171 +++++++++++++++++----------------- backend/redis/options.go | 76 +++++++++++++++ backend/redis/redis.go | 96 +++++++++++++++++++ docker-compose.yml | 37 ++++++++ examples/redis/redis.go | 58 ++++++++++++ go.mod | 4 +- go.sum | 70 ++------------ hypercache.go | 16 ++++ libs/serializer/json.go | 19 ++++ libs/serializer/serializer.go | 10 ++ models/item.go | 15 ++- utils/types.go | 2 +- 13 files changed, 428 insertions(+), 159 deletions(-) create mode 100644 backend/redis/options.go create mode 100644 backend/redis/redis.go create mode 100644 docker-compose.yml create mode 100644 examples/redis/redis.go create mode 100644 libs/serializer/json.go create mode 100644 libs/serializer/serializer.go diff --git a/backend/options.go b/backend/options.go index 72e1507..0bf86fd 100644 --- a/backend/options.go +++ b/backend/options.go @@ -1,7 +1,9 @@ package backend import ( - "github.com/go-redis/redis" + "fmt" + + "github.com/go-redis/redis/v8" "github.com/hyp3rd/hypercache/models" "github.com/hyp3rd/hypercache/types" ) @@ -26,7 +28,14 @@ func WithCapacity[T InMemory](capacity int) Option[InMemory] { // WithRedisClient is an option that sets the redis client to use. func WithRedisClient[T RedisBackend](client *redis.Client) Option[RedisBackend] { return func(backend *RedisBackend) { - backend.client = client + backend.rdb = client + } +} + +// WithPrefix is an option that sets the prefix to use for the keys of the items in the cache. +func WithPrefix[T RedisBackend](prefix string) Option[RedisBackend] { + return func(backend *RedisBackend) { + backend.prefix = fmt.Sprintf("%s:", prefix) } } diff --git a/backend/redis.go b/backend/redis.go index 13aaec8..f198756 100644 --- a/backend/redis.go +++ b/backend/redis.go @@ -1,20 +1,24 @@ package backend import ( + "context" "sort" "time" - "github.com/go-redis/redis" + "github.com/go-redis/redis/v8" "github.com/hyp3rd/hypercache/errors" + "github.com/hyp3rd/hypercache/libs/serializer" "github.com/hyp3rd/hypercache/models" "github.com/hyp3rd/hypercache/types" ) // RedisBackend is a cache backend that stores the items in a redis implementation. type RedisBackend struct { - client *redis.Client // redis client to interact with the redis server - capacity int // capacity of the cache, limits the number of items that can be stored in the cache - SortFilters // SortFilters holds the filters applied when listing the items in the cache + rdb *redis.Client // redis client to interact with the redis server + capacity int // capacity of the cache, limits the number of items that can be stored in the cache + prefix string // prefix is the prefix used to prefix the keys of the items in the cache + Serializer serializer.ISerializer // Serializer is the serializer used to serialize the items before storing them in the cache + SortFilters // SortFilters holds the filters applied when listing the items in the cache } // NewRedisBackend creates a new redis cache with the given options. @@ -24,7 +28,7 @@ func NewRedisBackend[T RedisBackend](redisOptions ...Option[RedisBackend]) (back ApplyOptions(rb, redisOptions...) // Check if the client is nil - if rb.client == nil { + if rb.rdb == nil { return nil, errors.ErrNilClient } // Check if the capacity is valid @@ -32,6 +36,12 @@ func NewRedisBackend[T RedisBackend](redisOptions ...Option[RedisBackend]) (back return nil, errors.ErrInvalidCapacity } + if rb.prefix == "" { + rb.prefix = "hypercache-" + } + + rb.Serializer = serializer.New() + // return the new backend return rb, nil } @@ -51,7 +61,7 @@ func (cacheBackend *RedisBackend) SetCapacity(capacity int) { // itemCount returns the number of items in the cache. func (cacheBackend *RedisBackend) itemCount() int { - count, _ := cacheBackend.client.DBSize().Result() + count, _ := cacheBackend.rdb.DBSize(context.Background()).Result() return int(count) } @@ -62,37 +72,63 @@ func (cacheBackend *RedisBackend) Size() int { // Get retrieves the Item with the given key from the cacheBackend. If the item is not found, it returns nil. func (cacheBackend *RedisBackend) Get(key string) (item *models.Item, ok bool) { - data, err := cacheBackend.client.HGetAll(key).Result() + // Get the item from the cacheBackend + item = models.ItemPool.Get().(*models.Item) + // fmt.Println(fmt.Sprintf("%s%s", cacheBackend.prefix, key)) + data, err := cacheBackend.rdb.HGet(context.Background(), key, "data").Bytes() if err != nil { + // Check if the item is not found if err == redis.Nil { return nil, false } return nil, false } - - item = models.ItemPool.Get().(*models.Item) - item.UnmarshalBinary([]byte(data["data"])) - item.Expiration, _ = time.ParseDuration(data["expiration"]) - + // Deserialize the item + err = cacheBackend.Serializer.Deserialize(data, item) + if err != nil { + return nil, false + } return item, true } +// generateKey +// @param key +// func generateKey(key string) string { +// hash := fnv.New64a() +// _, _ = hash.Write([]byte(key)) + +// return strconv.FormatUint(hash.Sum64(), 36) +// } + // Set stores the Item in the cacheBackend. func (cacheBackend *RedisBackend) Set(item *models.Item) error { + // Check if the item is valid if err := item.Valid(); err != nil { return err } - data, _ := item.MarshalBinary() + // Serialize the item + data, err := cacheBackend.Serializer.Serialize(item) + if err != nil { + return err + } + expiration := item.Expiration.String() - cacheBackend.client.HMSet(item.Key, map[string]interface{}{ + // Set the item in the cacheBackend + // fmt.Println(fmt.Sprintf("%s%s", cacheBackend.prefix, item.Key)) + err = cacheBackend.rdb.HSet(context.Background(), item.Key, map[string]interface{}{ "data": data, "expiration": expiration, - }) + }).Err() + + if err != nil { + return err + } - if item.Expiration >= 0 { - cacheBackend.client.Expire(item.Key, item.Expiration) + // Set the expiration if it is not zero + if item.Expiration > 0 { + cacheBackend.rdb.Expire(context.Background(), item.Key, item.Expiration) } return nil @@ -103,22 +139,53 @@ func (cacheBackend *RedisBackend) List(options ...FilterOption[RedisBackend]) ([ // Apply the filter options ApplyFilterOptions(cacheBackend, options...) - // Get all keys - keys, _ := cacheBackend.client.HKeys("*").Result() + var ( + cursor uint64 // cursor used to iterate over the keys + keys []string // keys of the items in the cacheBackend + err error // error returned by the redis client + ) + + // Iterate over the keys in the cacheBackend + for { + var nextCursor uint64 // next cursor returned by the redis client + // Scan the keys in the cacheBackend + // fmt.Println(fmt.Sprintf("%s*", cacheBackend.prefix)) + keys, nextCursor, err = cacheBackend.rdb.Scan(context.Background(), cursor, "*", 100).Result() + if err != nil { + return nil, err + } + // Update the cursor + cursor = nextCursor + if cursor == 0 { + // No more keys to iterate over + break + } + } + // Create a slice to hold the items items := make([]*models.Item, 0, len(keys)) + // Iterate over the keys and retrieve the items for _, key := range keys { + // Get the item from the cacheBackend + // fmt.Println("gettimg key", key) item, ok := cacheBackend.Get(key) if !ok { continue } + // Check if the item matches the filter options if cacheBackend.FilterFunc != nil && !cacheBackend.FilterFunc(item) { continue } items = append(items, item) } + // Sort the items + if cacheBackend.SortBy == "" { + // No sorting + return items, nil + } + sort.Slice(items, func(i, j int) bool { a := items[i].FieldByName(cacheBackend.SortBy) b := items[j].FieldByName(cacheBackend.SortBy) @@ -156,76 +223,14 @@ func (cacheBackend *RedisBackend) List(options ...FilterOption[RedisBackend]) ([ return items, nil } -// func (cacheBackend *RedisBackend) List(options ...FilterOption[RedisBackend]) ([]*models.Item, error) { -// // Apply the filter options -// ApplyFilterOptions(cacheBackend, options...) - -// // Get all keys -// keys, _ := cacheBackend.client.Keys("*").Result() - -// items := make([]*models.Item, 0, len(keys)) - -// for _, key := range keys { -// item, ok := cacheBackend.Get(key) -// if !ok { -// continue -// } -// if cacheBackend.FilterFunc != nil && !cacheBackend.FilterFunc(item) { -// continue -// } -// items = append(items, item) -// } - -// // Sort items -// if cacheBackend.SortBy == "" { -// return items, nil -// } - -// sort.Slice(items, func(i, j int) bool { -// a := items[i].FieldByName(cacheBackend.SortBy) -// b := items[j].FieldByName(cacheBackend.SortBy) -// switch cacheBackend.SortBy { -// case types.SortByKey.String(): -// if cacheBackend.SortAscending { -// return a.Interface().(string) < b.Interface().(string) -// } -// return a.Interface().(string) > b.Interface().(string) -// case types.SortByValue.String(): -// if cacheBackend.SortAscending { -// return a.Interface().(string) < b.Interface().(string) -// } -// return a.Interface().(string) > b.Interface().(string) -// case types.SortByLastAccess.String(): -// if cacheBackend.SortAscending { -// return a.Interface().(time.Time).Before(b.Interface().(time.Time)) -// } -// return a.Interface().(time.Time).After(b.Interface().(time.Time)) -// case types.SortByAccessCount.String(): -// if cacheBackend.SortAscending { -// return a.Interface().(uint) < b.Interface().(uint) -// } -// return a.Interface().(uint) > b.Interface().(uint) -// case types.SortByExpiration.String(): -// if cacheBackend.SortAscending { -// return a.Interface().(time.Duration) < b.Interface().(time.Duration) -// } -// return a.Interface().(time.Duration) > b.Interface().(time.Duration) -// default: -// return false -// } -// }) - -// return items, nil -// } - // Remove removes an item from the cache with the given key func (cacheBackend *RedisBackend) Remove(keys ...string) error { - _, err := cacheBackend.client.Del(keys...).Result() + _, err := cacheBackend.rdb.Del(context.Background(), keys...).Result() return err } // Clear removes all items from the cache func (cacheBackend *RedisBackend) Clear() error { - _, err := cacheBackend.client.FlushDB().Result() + _, err := cacheBackend.rdb.FlushDB(context.Background()).Result() return err } diff --git a/backend/redis/options.go b/backend/redis/options.go new file mode 100644 index 0000000..8a21216 --- /dev/null +++ b/backend/redis/options.go @@ -0,0 +1,76 @@ +package redis + +import ( + "crypto/tls" + "time" + + "github.com/go-redis/redis/v8" +) + +type Option func(*redis.Options) + +func ApplyOptions(opt *redis.Options, options ...Option) { + for _, option := range options { + option(opt) + } +} + +func WithAddr(addr string) Option { + return func(opt *redis.Options) { + opt.Addr = addr + } +} + +func WithPassword(password string) Option { + return func(opt *redis.Options) { + opt.Password = password + } +} + +func WithDB(db int) Option { + return func(opt *redis.Options) { + opt.DB = db + } +} + +func WithMaxRetries(maxRetries int) Option { + return func(opt *redis.Options) { + opt.MaxRetries = maxRetries + } +} + +func WithDialTimeout(dialTimeout time.Duration) Option { + return func(opt *redis.Options) { + opt.DialTimeout = dialTimeout + } +} + +func WithReadTimeout(readTimeout time.Duration) Option { + return func(opt *redis.Options) { + opt.ReadTimeout = readTimeout + } +} + +func WithWriteTimeout(writeTimeout time.Duration) Option { + return func(opt *redis.Options) { + opt.WriteTimeout = writeTimeout + } +} + +func WithPoolFIFO(poolFIFO bool) Option { + return func(opt *redis.Options) { + opt.PoolFIFO = poolFIFO + } +} + +func WithPoolSize(poolSize int) Option { + return func(opt *redis.Options) { + opt.PoolSize = poolSize + } +} + +func WithTLSConfig(tlsConfig *tls.Config) Option { + return func(opt *redis.Options) { + opt.TLSConfig = tlsConfig + } +} diff --git a/backend/redis/redis.go b/backend/redis/redis.go new file mode 100644 index 0000000..9722f04 --- /dev/null +++ b/backend/redis/redis.go @@ -0,0 +1,96 @@ +package redis + +import ( + "fmt" + "time" + + "github.com/go-redis/redis/v8" +) + +type Store struct { + Client *redis.Client +} + +// New +// @param opt +func New(opt *redis.Options) *Store { + cli := redis.NewClient(opt) + + return &Store{Client: cli} +} + +func NewStore(opts ...Option) (*Store, error) { + // Setup redis client + opt := &redis.Options{ + MaxRetries: 10, + DialTimeout: 10 * time.Second, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + PoolFIFO: false, + PoolSize: 10, + } + + ApplyOptions(opt, opts...) + + if opt.Addr == "" { + return nil, fmt.Errorf("redis address is empty") + } + + cli := redis.NewClient(opt) + + return &Store{Client: cli}, nil +} + +// NewWithDb +// @param tx +func NewWithDb(tx *redis.Client) *Store { + return &Store{Client: tx} +} + +// func NewOptions() (opt *redis.Options, err error) { + +// // read options from config +// opt = &redis.Options{ +// Addr: config.Redis.Address, +// Password: config.Redis.Password, +// DB: config.Redis.DB, +// MaxRetries: 10, +// DialTimeout: 10 * time.Second, +// ReadTimeout: 30 * time.Second, +// WriteTimeout: 30 * time.Second, +// PoolFIFO: false, +// PoolSize: 10, +// } + +// // read CA cert +// caPath, ok := os.LookupEnv("TLS_CA_CERT_FILE") +// if ok && caPath != "" { +// // ref https://pkg.go.dev/crypto/tls#example-Dial +// rootCertPool := x509.NewCertPool() +// pem, err := ioutil.ReadFile(caPath) +// if err != nil { +// return nil, err +// } +// if ok := rootCertPool.AppendCertsFromPEM(pem); !ok { +// return nil, fmt.Errorf("failed to append root CA cert at %s", caPath) +// } +// opt.TLSConfig = &tls.Config{ +// RootCAs: rootCertPool, +// } + +// // https://pkg.go.dev/crypto/tls#LoadX509KeyPair +// clientCert, ok := os.LookupEnv("TLS_CERT_FILE") +// clientKey, ok2 := os.LookupEnv("TLS_KEY_FILE") +// if ok && ok2 { +// cert, err := tls.LoadX509KeyPair(clientCert, clientKey) +// if err != nil { +// return nil, err +// } +// opt.TLSConfig.Certificates = []tls.Certificate{cert} +// opt.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert +// opt.TLSConfig.ClientCAs = rootCertPool +// } +// } + +// return opt, nil +// } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e2f023b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,37 @@ +version: "3.9" + +x-logging: &default-logging + options: + max-size: "12m" + max-file: "5" + driver: json-file + +services: + ####################################### + # The redis-store backend caching service. + ####################################### + redis-store: + command: + - /opt/bitnami/scripts/redis/run.sh + - --maxmemory + - 256mb + container_name: redis-store + environment: + ALLOW_EMPTY_PASSWORD: "no" + REDIS_DISABLE_COMMANDS: FLUSHDB,FLUSHALL,CONFIG + REDIS_PASSWORD: k7oMs2G5bc4mRN45jPZjLBZxuMFrCLahvPn648Zwq1lT41gSYZqapBRnSF2L995FaYcZBz8c7xkKXku94HeReDgdwBu1N4CzgfQ94Z504hjfzrST1u0idVkbXe8ust + hostname: redis-store + image: bitnami/redis:7.0.7 + # networks: + # hypercache-redis-store-net: null + ####################################### + ports: # uncomment to be able to bypass traefik + - mode: ingress + target: 6379 + published: 6379 + protocol: tcp +# Networks section +networks: + hypercache-redis-store-net: + internal: true + name: hypercache-redis-store-net \ No newline at end of file diff --git a/examples/redis/redis.go b/examples/redis/redis.go new file mode 100644 index 0000000..f2fed40 --- /dev/null +++ b/examples/redis/redis.go @@ -0,0 +1,58 @@ +package main + +import ( + "fmt" + "time" + + "github.com/hyp3rd/hypercache" + "github.com/hyp3rd/hypercache/backend" + "github.com/hyp3rd/hypercache/backend/redis" +) + +func main() { + redisStore, err := redis.NewStore( + redis.WithAddr("localhost:6379"), + redis.WithPassword("k7oMs2G5bc4mRN45jPZjLBZxuMFrCLahvPn648Zwq1lT41gSYZqapBRnSF2L995FaYcZBz8c7xkKXku94HeReDgdwBu1N4CzgfQ94Z504hjfzrST1u0idVkbXe8ust"), + redis.WithDB(0), + ) + + if err != nil { + panic(err) + } + + conf := &hypercache.Config[backend.RedisBackend]{ + RedisOptions: []backend.Option[backend.RedisBackend]{ + backend.WithRedisClient(redisStore.Client), + }, + } + + hyperCache, err := hypercache.New(conf) + if err != nil { + panic(err) + } + + for i := 0; i < 400; i++ { + err = hyperCache.Set(fmt.Sprintf("key-%d", i), fmt.Sprintf("value-%d", i), time.Hour) + if err != nil { + panic(err) + } + } + + value, ok := hyperCache.Get("key-100") + + if !ok { + fmt.Println("key not found") + } + + allItems, err := hyperCache.List() + if err != nil { + panic(err) + } + + for _, item := range allItems { + fmt.Println(item.Key, item.Value) + } + + fmt.Println(value) + +} diff --git a/go.mod b/go.mod index 7dfab79..fd0dee1 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.19 require ( github.com/go-redis/redis v6.15.9+incompatible + github.com/go-redis/redis/v8 v8.11.5 github.com/google/go-cmp v0.5.9 github.com/longbridgeapp/assert v1.1.0 github.com/shamaton/msgpack/v2 v2.1.1 @@ -11,8 +12,9 @@ require ( ) require ( + github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/onsi/ginkgo v1.16.5 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/onsi/gomega v1.24.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/testify v1.8.0 // indirect diff --git a/go.sum b/go.sum index fdeca8d..57e707f 100644 --- a/go.sum +++ b/go.sum @@ -1,37 +1,22 @@ github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/longbridgeapp/assert v1.1.0 h1:L+/HISOhuGbNAAmJNXgk3+Tm5QmSB70kwdktJXgjL+I= github.com/longbridgeapp/assert v1.1.0/go.mod h1:UOI7O3rzlzlz715lQm0atWs6JbrYGuIJUEeOekutL6o= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.24.2 h1:J/tulyYK6JwBldPViHJReihxxZ+22FHs0piGjQAvoUE= github.com/onsi/gomega v1.24.2/go.mod h1:gs3J10IS7Z7r7eXRoNJIrNqU4ToQukCJhFtKrWgHWnk= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= @@ -42,11 +27,9 @@ github.com/shamaton/msgpack/v2 v2.1.1/go.mod h1:aTUEmh31ziGX1Ml7wMPLVY0f4vT3CRsC github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= @@ -54,53 +37,12 @@ go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/hypercache.go b/hypercache.go index 8578fdd..7ffaf94 100644 --- a/hypercache.go +++ b/hypercache.go @@ -485,6 +485,11 @@ func (hyperCache *HyperCache[T]) List(filters ...any) ([]*models.Item, error) { listInstance = listInMemory(hyperCache.backend.(*backend.InMemory)) } + if hyperCache.cacheBackendChecker.IsRedis() { + // if the backend is a Redis, we set the listFunc to the ListRedis function + listInstance = listRedis(hyperCache.backend.(*backend.RedisBackend)) + } + // calling the corresponding implementation of the list function return listInstance(filters...) } @@ -507,6 +512,17 @@ func listInMemory(cacheBackend *backend.InMemory) listFunc { } } +func listRedis(cacheBackend *backend.RedisBackend) listFunc { + return func(options ...any) ([]*models.Item, error) { + // here we are converting the filters of any type to the specific FilterOption type for the Redis + filterOptions := make([]backend.FilterOption[backend.RedisBackend], len(options)) + for i, option := range options { + filterOptions[i] = option.(backend.FilterOption[backend.RedisBackend]) + } + return cacheBackend.List(filterOptions...) + } +} + // Remove removes items with the given key from the cache. If an item is not found, it does nothing. func (hyperCache *HyperCache[T]) Remove(keys ...string) { hyperCache.backend.Remove(keys...) diff --git a/libs/serializer/json.go b/libs/serializer/json.go new file mode 100644 index 0000000..274c8f1 --- /dev/null +++ b/libs/serializer/json.go @@ -0,0 +1,19 @@ +package serializer + +import "encoding/json" + +type DefaultJSONSerializer struct { +} + +// Serialize +// @param v +func (d *DefaultJSONSerializer) Serialize(v any) ([]byte, error) { + return json.Marshal(v) +} + +// Deserialize +// @param data +// @param v +func (d *DefaultJSONSerializer) Deserialize(data []byte, v any) error { + return json.Unmarshal(data, v) +} diff --git a/libs/serializer/serializer.go b/libs/serializer/serializer.go new file mode 100644 index 0000000..56f9ea1 --- /dev/null +++ b/libs/serializer/serializer.go @@ -0,0 +1,10 @@ +package serializer + +type ISerializer interface { + Serialize(v any) ([]byte, error) + Deserialize(data []byte, v any) error +} + +func New() ISerializer { + return &DefaultJSONSerializer{} +} diff --git a/models/item.go b/models/item.go index 2b5b8b1..ad287c4 100644 --- a/models/item.go +++ b/models/item.go @@ -11,7 +11,6 @@ import ( // "https://github.com/kelindar/binary" "github.com/hyp3rd/hypercache/errors" - "github.com/shamaton/msgpack/v2" ) // Item is a struct that represents an item in the cache. It has a key, value, expiration duration, and a last access time field. @@ -100,11 +99,11 @@ func (item *Item) Expired() bool { // // MarshalBinary implements the encoding.BinaryMarshaler interface. -func (item *Item) MarshalBinary() (data []byte, err error) { - return msgpack.Marshal(item) -} +// func (item *Item) MarshalBinary() (data []byte, err error) { +// return msgpack.Marshal(item) +// } -// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. -func (item *Item) UnmarshalBinary(data []byte) error { - return msgpack.Unmarshal(data, item) -} +// // UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +// func (item *Item) UnmarshalBinary(data []byte) error { +// return msgpack.Unmarshal(data, item) +// } diff --git a/utils/types.go b/utils/types.go index c56c23c..22988b2 100644 --- a/utils/types.go +++ b/utils/types.go @@ -33,7 +33,7 @@ func (c *CacheBackendChecker[T]) IsInMemory() bool { } // IsRedisBackend returns true if the backend is a RedisBackend -func (c *CacheBackendChecker[T]) IsRedisBackend() bool { +func (c *CacheBackendChecker[T]) IsRedis() bool { _, ok := c.Backend.(*backend.RedisBackend) return ok } From 89bf02fadc29d097258ba2c2cb4b50ecdbf1ca30 Mon Sep 17 00:00:00 2001 From: Francesco Cosentino Date: Sun, 15 Jan 2023 20:29:49 +0100 Subject: [PATCH 2/5] redis implementation --- backend/options.go | 37 ++++-- backend/redis.go | 178 +++++++++++++++------------ backend/redis/{redis.go => store.go} | 0 examples/redis/redis.go | 6 +- 4 files changed, 133 insertions(+), 88 deletions(-) rename backend/redis/{redis.go => store.go} (100%) diff --git a/backend/options.go b/backend/options.go index 0bf86fd..a7f047c 100644 --- a/backend/options.go +++ b/backend/options.go @@ -1,8 +1,6 @@ package backend import ( - "fmt" - "github.com/go-redis/redis/v8" "github.com/hyp3rd/hypercache/models" "github.com/hyp3rd/hypercache/types" @@ -32,10 +30,10 @@ func WithRedisClient[T RedisBackend](client *redis.Client) Option[RedisBackend] } } -// WithPrefix is an option that sets the prefix to use for the keys of the items in the cache. -func WithPrefix[T RedisBackend](prefix string) Option[RedisBackend] { +// WithKeysSetName is an option that sets the name of the set that holds the keys of the items in the cache +func WithKeysSetName[T RedisBackend](keysSetName string) Option[RedisBackend] { return func(backend *RedisBackend) { - backend.prefix = fmt.Sprintf("%s:", prefix) + backend.keysSetName = keysSetName } } @@ -43,7 +41,7 @@ func WithPrefix[T RedisBackend](prefix string) Option[RedisBackend] { type FilterOption[T any] func(*T) // ApplyFilterOptions applies the given options to the given filter. -func ApplyFilterOptions[T any](backend *T, options ...FilterOption[T]) { +func ApplyFilterOptions[T IBackendConstrain](backend *T, options ...FilterOption[T]) { for _, option := range options { option(backend) } @@ -51,33 +49,54 @@ func ApplyFilterOptions[T any](backend *T, options ...FilterOption[T]) { // WithSortBy is an option that sets the field to sort the items by. // The field can be any of the fields in the `Item` struct. -func WithSortBy[T any](field types.SortingField) FilterOption[T] { +func WithSortBy[T IBackendConstrain](field types.SortingField) FilterOption[T] { return func(a *T) { switch filter := any(a).(type) { case *InMemory: filter.SortBy = field.String() + case *RedisBackend: + filter.SortBy = field.String() } } } // WithSortAscending is an option that sets the sort order to ascending. // When sorting the items in the cache, they will be sorted in ascending order based on the field specified with the `WithSortBy` option. -func WithSortAscending[T any]() FilterOption[T] { +// Deprecated: Use `WithSortOrderAsc` instead. +func WithSortAscending[T IBackendConstrain]() FilterOption[T] { return func(a *T) { switch filter := any(a).(type) { case *InMemory: filter.SortAscending = true + case *RedisBackend: + filter.SortAscending = true } } } // WithSortDescending is an option that sets the sort order to descending. // When sorting the items in the cache, they will be sorted in descending order based on the field specified with the `WithSortBy` option. -func WithSortDescending[T any]() FilterOption[T] { +// Deprecated: Use `WithSortOrderAsc` instead. +func WithSortDescending[T IBackendConstrain]() FilterOption[T] { return func(a *T) { switch filter := any(a).(type) { case *InMemory: filter.SortAscending = false + case *RedisBackend: + filter.SortAscending = false + } + } +} + +// WithSortOrderAsc is an option that sets the sort order to ascending or descending. +// When sorting the items in the cache, they will be sorted in ascending or descending order based on the field specified with the `WithSortBy` option. +func WithSortOrderAsc[T IBackendConstrain](ascending bool) FilterOption[T] { + return func(a *T) { + switch filter := any(a).(type) { + case *InMemory: + filter.SortAscending = ascending + case *RedisBackend: + filter.SortAscending = ascending } } } diff --git a/backend/redis.go b/backend/redis.go index f198756..935526b 100644 --- a/backend/redis.go +++ b/backend/redis.go @@ -2,8 +2,8 @@ package backend import ( "context" + "fmt" "sort" - "time" "github.com/go-redis/redis/v8" "github.com/hyp3rd/hypercache/errors" @@ -16,7 +16,7 @@ import ( type RedisBackend struct { rdb *redis.Client // redis client to interact with the redis server capacity int // capacity of the cache, limits the number of items that can be stored in the cache - prefix string // prefix is the prefix used to prefix the keys of the items in the cache + keysSetName string // keysSetName is the name of the set that holds the keys of the items in the cache Serializer serializer.ISerializer // Serializer is the serializer used to serialize the items before storing them in the cache SortFilters // SortFilters holds the filters applied when listing the items in the cache } @@ -36,8 +36,8 @@ func NewRedisBackend[T RedisBackend](redisOptions ...Option[RedisBackend]) (back return nil, errors.ErrInvalidCapacity } - if rb.prefix == "" { - rb.prefix = "hypercache-" + if rb.keysSetName == "" { + rb.keysSetName = "hypercache" } rb.Serializer = serializer.New() @@ -72,9 +72,17 @@ func (cacheBackend *RedisBackend) Size() int { // Get retrieves the Item with the given key from the cacheBackend. If the item is not found, it returns nil. func (cacheBackend *RedisBackend) Get(key string) (item *models.Item, ok bool) { + // Check if the key is in the set of keys + isMember, err := cacheBackend.rdb.SIsMember(context.Background(), cacheBackend.keysSetName, key).Result() + if err != nil { + return nil, false + } + if !isMember { + return nil, false + } + // Get the item from the cacheBackend item = models.ItemPool.Get().(*models.Item) - // fmt.Println(fmt.Sprintf("%s%s", cacheBackend.prefix, key)) data, err := cacheBackend.rdb.HGet(context.Background(), key, "data").Bytes() if err != nil { // Check if the item is not found @@ -91,15 +99,6 @@ func (cacheBackend *RedisBackend) Get(key string) (item *models.Item, ok bool) { return item, true } -// generateKey -// @param key -// func generateKey(key string) string { -// hash := fnv.New64a() -// _, _ = hash.Write([]byte(key)) - -// return strconv.FormatUint(hash.Sum64(), 36) -// } - // Set stores the Item in the cacheBackend. func (cacheBackend *RedisBackend) Set(item *models.Item) error { // Check if the item is valid @@ -115,8 +114,7 @@ func (cacheBackend *RedisBackend) Set(item *models.Item) error { expiration := item.Expiration.String() - // Set the item in the cacheBackend - // fmt.Println(fmt.Sprintf("%s%s", cacheBackend.prefix, item.Key)) + // Store the item in the cacheBackend err = cacheBackend.rdb.HSet(context.Background(), item.Key, map[string]interface{}{ "data": data, "expiration": expiration, @@ -126,6 +124,9 @@ func (cacheBackend *RedisBackend) Set(item *models.Item) error { return err } + // Add the key to the set of keys associated with the cache prefix + cacheBackend.rdb.SAdd(context.Background(), cacheBackend.keysSetName, item.Key) + // Set the expiration if it is not zero if item.Expiration > 0 { cacheBackend.rdb.Expire(context.Background(), item.Key, item.Expiration) @@ -139,27 +140,10 @@ func (cacheBackend *RedisBackend) List(options ...FilterOption[RedisBackend]) ([ // Apply the filter options ApplyFilterOptions(cacheBackend, options...) - var ( - cursor uint64 // cursor used to iterate over the keys - keys []string // keys of the items in the cacheBackend - err error // error returned by the redis client - ) - - // Iterate over the keys in the cacheBackend - for { - var nextCursor uint64 // next cursor returned by the redis client - // Scan the keys in the cacheBackend - // fmt.Println(fmt.Sprintf("%s*", cacheBackend.prefix)) - keys, nextCursor, err = cacheBackend.rdb.Scan(context.Background(), cursor, "*", 100).Result() - if err != nil { - return nil, err - } - // Update the cursor - cursor = nextCursor - if cursor == 0 { - // No more keys to iterate over - break - } + // Get the set of keys + keys, err := cacheBackend.rdb.SMembers(context.Background(), cacheBackend.keysSetName).Result() + if err != nil { + return nil, err } // Create a slice to hold the items @@ -168,16 +152,14 @@ func (cacheBackend *RedisBackend) List(options ...FilterOption[RedisBackend]) ([ // Iterate over the keys and retrieve the items for _, key := range keys { // Get the item from the cacheBackend - // fmt.Println("gettimg key", key) item, ok := cacheBackend.Get(key) - if !ok { - continue - } - // Check if the item matches the filter options - if cacheBackend.FilterFunc != nil && !cacheBackend.FilterFunc(item) { - continue + if ok { + // Check if the item matches the filter options + if cacheBackend.FilterFunc != nil && !cacheBackend.FilterFunc(item) { + continue + } + items = append(items, item) } - items = append(items, item) } // Sort the items @@ -186,43 +168,83 @@ func (cacheBackend *RedisBackend) List(options ...FilterOption[RedisBackend]) ([ return items, nil } - sort.Slice(items, func(i, j int) bool { - a := items[i].FieldByName(cacheBackend.SortBy) - b := items[j].FieldByName(cacheBackend.SortBy) - switch cacheBackend.SortBy { - case types.SortByKey.String(): - if cacheBackend.SortAscending { - return a.Interface().(string) < b.Interface().(string) - } - return a.Interface().(string) > b.Interface().(string) - case types.SortByValue.String(): - if cacheBackend.SortAscending { - return a.Interface().(string) < b.Interface().(string) - } - return a.Interface().(string) > b.Interface().(string) - case types.SortByLastAccess.String(): - if cacheBackend.SortAscending { - return a.Interface().(time.Time).Before(b.Interface().(time.Time)) - } - return a.Interface().(time.Time).After(b.Interface().(time.Time)) - case types.SortByAccessCount.String(): - if cacheBackend.SortAscending { - return a.Interface().(uint) < b.Interface().(uint) - } - return a.Interface().(uint) > b.Interface().(uint) - case types.SortByExpiration.String(): - if cacheBackend.SortAscending { - return a.Interface().(time.Duration) < b.Interface().(time.Duration) - } - return a.Interface().(time.Duration) > b.Interface().(time.Duration) - default: - return false - } - }) + var sorter sort.Interface + switch cacheBackend.SortBy { + case types.SortByKey.String(): + sorter = &itemSorterByKey{items: items} + case types.SortByValue.String(): + sorter = &itemSorterByValue{items: items} + case types.SortByLastAccess.String(): + sorter = &itemSorterByLastAccess{items: items} + case types.SortByAccessCount.String(): + sorter = &itemSorterByAccessCount{items: items} + case types.SortByExpiration.String(): + sorter = &itemSorterByExpiration{items: items} + default: + return nil, fmt.Errorf("unknown sortBy field: %s", cacheBackend.SortBy) + } + + if !cacheBackend.SortAscending { + sorter = sort.Reverse(sorter) + } + + sort.Sort(sorter) return items, nil } +// func (cacheBackend *RedisBackend) List(options ...FilterOption[RedisBackend]) ([]*models.Item, error) { +// // Apply the filter options +// // Apply the filter options +// ApplyFilterOptions(cacheBackend, options...) + +// // Initialize the score range +// var min, max string +// if cacheBackend.SortAscending { +// min = "-inf" +// max = "+inf" +// } else { +// min = "+inf" +// max = "-inf" +// } + +// // Get the keys from the sorted set +// var keys []string +// var err error +// zrangeby := &redis.ZRangeBy{ +// Min: min, Max: max, +// } +// if cacheBackend.SortBy == types.SortByLastAccess.String() { +// keys, err = cacheBackend.rdb.ZRangeByScore(context.Background(), "last_access_sort", zrangeby).Result() +// } else if cacheBackend.SortBy == types.SortByAccessCount.String() { +// keys, err = cacheBackend.rdb.ZRangeByScore(context.Background(), "access_count_sort", zrangeby).Result() +// } else { +// return nil, fmt.Errorf("invalid sorting criteria: %s", cacheBackend.SortBy) +// } +// if err != nil { +// return nil, err +// } + +// // Create a slice to hold the items +// items := make([]*models.Item, 0, len(keys)) + +// // Iterate over the keys and retrieve the items +// for _, key := range keys { +// // Get the item from the cacheBackend +// item, ok := cacheBackend.Get(key) +// if !ok { +// continue +// } +// // Check if the item matches the filter options +// if cacheBackend.FilterFunc != nil && !cacheBackend.FilterFunc(item) { +// continue +// } +// items = append(items, item) +// } + +// return items, nil +// } + // Remove removes an item from the cache with the given key func (cacheBackend *RedisBackend) Remove(keys ...string) error { _, err := cacheBackend.rdb.Del(context.Background(), keys...).Result() diff --git a/backend/redis/redis.go b/backend/redis/store.go similarity index 100% rename from backend/redis/redis.go rename to backend/redis/store.go diff --git a/examples/redis/redis.go b/examples/redis/redis.go index f2fed40..fbcb46d 100644 --- a/examples/redis/redis.go +++ b/examples/redis/redis.go @@ -7,6 +7,7 @@ import ( "github.com/hyp3rd/hypercache" "github.com/hyp3rd/hypercache/backend" "github.com/hyp3rd/hypercache/backend/redis" + "github.com/hyp3rd/hypercache/types" ) func main() { @@ -44,7 +45,10 @@ func main() { fmt.Println("key not found") } - allItems, err := hyperCache.List() + allItems, err := hyperCache.List( + backend.WithSortBy[backend.RedisBackend](types.SortByValue), + backend.WithSortOrderAsc[backend.RedisBackend](true), + ) if err != nil { panic(err) } From 0257a0875774a58d94830384726bd277379abf52 Mon Sep 17 00:00:00 2001 From: Francesco Cosentino Date: Sun, 15 Jan 2023 22:57:30 +0100 Subject: [PATCH 3/5] improved backend Options --- backend/options.go | 100 ++++++++++++++++++++++++---------------- examples/list/list.go | 14 +++--- examples/redis/redis.go | 20 +++++--- examples/stats/stats.go | 2 +- 4 files changed, 80 insertions(+), 56 deletions(-) diff --git a/backend/options.go b/backend/options.go index a7f047c..76507c3 100644 --- a/backend/options.go +++ b/backend/options.go @@ -6,6 +6,52 @@ import ( "github.com/hyp3rd/hypercache/types" ) +// ISortableBackend is an interface that defines the methods that a backend should implement to be sortable. +type iSortableBackend interface { + // setSortAscending indicates whether the items should be sorted in ascending order. + setSortAscending(ascending bool) + // setSortBy sets the field to sort the items by. + setSortBy(sortBy string) +} + +// setSortAscending sets the `SortAscending` field of the `InMemory` backend. +func (inm *InMemory) setSortAscending(ascending bool) { + inm.SortAscending = ascending +} + +// setSortAscending sets the `SortAscending` field of the `RedisBackend` backend. +func (rb *RedisBackend) setSortAscending(ascending bool) { + rb.SortAscending = ascending +} + +// setSortBy sets the `SortBy` field of the `InMemory` backend. +func (inm *InMemory) setSortBy(sortBy string) { + inm.SortBy = sortBy +} + +// setSortBy sets the `SortBy` field of the `RedisBackend` backend. +func (rb *RedisBackend) setSortBy(sortBy string) { + rb.SortBy = sortBy +} + +// FilterFunc is a predicate that takes a `Item` as an argument and returns a boolean indicating whether the item should be included in the cache. +type FilterFunc func(item *models.Item) bool // filters applied when listing the items in the cache + +// IFilterableBackend is an interface that defines the methods that a backend should implement to be filterable. +type IFilterableBackend interface { + setFilterFunc(filterFunc FilterFunc) +} + +// setFilterFunc sets the `FilterFunc` field of the `InMemory` backend. +func (inm *InMemory) setFilterFunc(filterFunc FilterFunc) { + inm.FilterFunc = filterFunc +} + +// setFilterFunc sets the `FilterFunc` field of the `RedisBackend` backend. +func (rb *RedisBackend) setFilterFunc(filterFunc FilterFunc) { + rb.FilterFunc = filterFunc +} + // Option is a function type that can be used to configure the `HyperCache` struct. type Option[T IBackendConstrain] func(*T) @@ -51,39 +97,8 @@ func ApplyFilterOptions[T IBackendConstrain](backend *T, options ...FilterOption // The field can be any of the fields in the `Item` struct. func WithSortBy[T IBackendConstrain](field types.SortingField) FilterOption[T] { return func(a *T) { - switch filter := any(a).(type) { - case *InMemory: - filter.SortBy = field.String() - case *RedisBackend: - filter.SortBy = field.String() - } - } -} - -// WithSortAscending is an option that sets the sort order to ascending. -// When sorting the items in the cache, they will be sorted in ascending order based on the field specified with the `WithSortBy` option. -// Deprecated: Use `WithSortOrderAsc` instead. -func WithSortAscending[T IBackendConstrain]() FilterOption[T] { - return func(a *T) { - switch filter := any(a).(type) { - case *InMemory: - filter.SortAscending = true - case *RedisBackend: - filter.SortAscending = true - } - } -} - -// WithSortDescending is an option that sets the sort order to descending. -// When sorting the items in the cache, they will be sorted in descending order based on the field specified with the `WithSortBy` option. -// Deprecated: Use `WithSortOrderAsc` instead. -func WithSortDescending[T IBackendConstrain]() FilterOption[T] { - return func(a *T) { - switch filter := any(a).(type) { - case *InMemory: - filter.SortAscending = false - case *RedisBackend: - filter.SortAscending = false + if sortable, ok := any(a).(iSortableBackend); ok { + sortable.setSortBy(field.String()) } } } @@ -92,11 +107,8 @@ func WithSortDescending[T IBackendConstrain]() FilterOption[T] { // When sorting the items in the cache, they will be sorted in ascending or descending order based on the field specified with the `WithSortBy` option. func WithSortOrderAsc[T IBackendConstrain](ascending bool) FilterOption[T] { return func(a *T) { - switch filter := any(a).(type) { - case *InMemory: - filter.SortAscending = ascending - case *RedisBackend: - filter.SortAscending = ascending + if sortable, ok := any(a).(iSortableBackend); ok { + sortable.setSortAscending(ascending) } } } @@ -105,9 +117,15 @@ func WithSortOrderAsc[T IBackendConstrain](ascending bool) FilterOption[T] { // The filter function is a predicate that takes a `Item` as an argument and returns a boolean indicating whether the item should be included in the cache. func WithFilterFunc[T any](fn func(item *models.Item) bool) FilterOption[T] { return func(a *T) { - switch filter := any(a).(type) { - case *InMemory: - filter.FilterFunc = fn + if filterable, ok := any(a).(IFilterableBackend); ok { + filterable.setFilterFunc(fn) } } + + // return func(a *T) { + // switch filter := any(a).(type) { + // case *InMemory: + // filter.FilterFunc = fn + // } + // } } diff --git a/examples/list/list.go b/examples/list/list.go index 2d2c22b..9f860a4 100644 --- a/examples/list/list.go +++ b/examples/list/list.go @@ -12,8 +12,8 @@ import ( // This example demonstrates how to list items from the cache func main() { - // Create a new HyperCache with a capacity of 100 - hyperCache, err := hypercache.NewInMemoryWithDefaults(100) + // Create a new HyperCache with a capacity of 400 + hyperCache, err := hypercache.NewInMemoryWithDefaults(400) if err != nil { fmt.Println(err) @@ -23,9 +23,9 @@ func main() { defer hyperCache.Stop() // Add 100 items to the cache - for i := 0; i < 100; i++ { + for i := 0; i < 400; i++ { key := fmt.Sprintf("key%d", i) - val := i //fmt.Sprintf("%d", i) + val := fmt.Sprintf("val%d", i) err = hyperCache.Set(key, val, time.Minute) @@ -37,10 +37,10 @@ func main() { // Retrieve the list of items from the cache list, err := hyperCache.List( - backend.WithSortBy[backend.InMemory](types.SortByValue), - backend.WithSortAscending[backend.InMemory](), + backend.WithSortBy[backend.InMemory](types.SortByKey), + backend.WithSortOrderAsc[backend.InMemory](true), backend.WithFilterFunc[backend.InMemory](func(item *models.Item) bool { - return item.Value != "val98" + return item.Value == "val98" }), ) diff --git a/examples/redis/redis.go b/examples/redis/redis.go index fbcb46d..85da598 100644 --- a/examples/redis/redis.go +++ b/examples/redis/redis.go @@ -7,6 +7,7 @@ import ( "github.com/hyp3rd/hypercache" "github.com/hyp3rd/hypercache/backend" "github.com/hyp3rd/hypercache/backend/redis" + "github.com/hyp3rd/hypercache/models" "github.com/hyp3rd/hypercache/types" ) @@ -39,24 +40,29 @@ func main() { } } - value, ok := hyperCache.Get("key-100") + // value, ok := hyperCache.Get("key-100") - if !ok { - fmt.Println("key not found") - } + // if !ok { + // fmt.Println("key not found") + // } allItems, err := hyperCache.List( - backend.WithSortBy[backend.RedisBackend](types.SortByValue), + backend.WithSortBy[backend.RedisBackend](types.SortByKey), backend.WithSortOrderAsc[backend.RedisBackend](true), + backend.WithFilterFunc[backend.RedisBackend](func(item *models.Item) bool { + return item.Value == "value-210" + }), ) + + // Check for errors if err != nil { panic(err) } + // Print the list of items for _, item := range allItems { fmt.Println(item.Key, item.Value) } - fmt.Println(value) - + // fmt.Println(value) } diff --git a/examples/stats/stats.go b/examples/stats/stats.go index 38fdc2b..3e8d170 100644 --- a/examples/stats/stats.go +++ b/examples/stats/stats.go @@ -52,7 +52,7 @@ func main() { // Retrieve the list of items from the cache list, err := hyperCache.List( backend.WithSortBy[backend.InMemory](types.SortByValue), - backend.WithSortDescending[backend.InMemory](), + backend.WithSortOrderAsc[backend.InMemory](true), backend.WithFilterFunc[backend.InMemory](func(item *models.Item) bool { return item.Expiration > time.Second }), From 0e47c2a31cb644454dca584d53bf2fac37a2237e Mon Sep 17 00:00:00 2001 From: Francesco Cosentino Date: Mon, 16 Jan 2023 19:55:38 +0100 Subject: [PATCH 4/5] redis backend and serializers, minor refactors --- backend/options.go | 13 +++- backend/redis.go | 135 +++++++++++++++------------------- backend/redis/options.go | 35 ++++++++- backend/redis/store.go | 82 ++++----------------- errors/errors.go | 3 + eviction/eviction.go | 20 ++--- examples/redis/redis.go | 10 +-- go.mod | 6 +- go.sum | 12 +-- hypercache.go | 4 +- libs/serializer/json.go | 13 ++-- libs/serializer/msgpack.go | 22 ++++++ libs/serializer/serializer.go | 46 +++++++++++- middleware/stats.go | 4 +- models/item.go | 27 ++++--- stats/collector.go | 12 +-- 16 files changed, 239 insertions(+), 205 deletions(-) create mode 100644 libs/serializer/msgpack.go diff --git a/backend/options.go b/backend/options.go index 76507c3..3d3ff4f 100644 --- a/backend/options.go +++ b/backend/options.go @@ -1,7 +1,8 @@ package backend import ( - "github.com/go-redis/redis/v8" + "github.com/go-redis/redis/v9" + "github.com/hyp3rd/hypercache/libs/serializer" "github.com/hyp3rd/hypercache/models" "github.com/hyp3rd/hypercache/types" ) @@ -83,6 +84,16 @@ func WithKeysSetName[T RedisBackend](keysSetName string) Option[RedisBackend] { } } +// WithSerializer is an option that sets the serializer to use. The serializer is used to serialize and deserialize the items in the cache. +// - The default serializer is `serializer.MsgpackSerializer`. +// - The `serializer.JSONSerializer` can be used to serialize and deserialize the items in the cache as JSON. +// - The interface `serializer.ISerializer` can be implemented to use a custom serializer. +func WithSerializer[T RedisBackend](serializer serializer.ISerializer) Option[RedisBackend] { + return func(backend *RedisBackend) { + backend.Serializer = serializer + } +} + // FilterOption is a function type that can be used to configure the `Filter` struct. type FilterOption[T any] func(*T) diff --git a/backend/redis.go b/backend/redis.go index 935526b..42f4eb4 100644 --- a/backend/redis.go +++ b/backend/redis.go @@ -5,7 +5,7 @@ import ( "fmt" "sort" - "github.com/go-redis/redis/v8" + "github.com/go-redis/redis/v9" "github.com/hyp3rd/hypercache/errors" "github.com/hyp3rd/hypercache/libs/serializer" "github.com/hyp3rd/hypercache/models" @@ -40,7 +40,10 @@ func NewRedisBackend[T RedisBackend](redisOptions ...Option[RedisBackend]) (back rb.keysSetName = "hypercache" } - rb.Serializer = serializer.New() + rb.Serializer, err = serializer.New("msgpack") + if err != nil { + return nil, err + } // return the new backend return rb, nil @@ -72,8 +75,10 @@ func (cacheBackend *RedisBackend) Size() int { // Get retrieves the Item with the given key from the cacheBackend. If the item is not found, it returns nil. func (cacheBackend *RedisBackend) Get(key string) (item *models.Item, ok bool) { + pipe := cacheBackend.rdb.TxPipeline() // Check if the key is in the set of keys - isMember, err := cacheBackend.rdb.SIsMember(context.Background(), cacheBackend.keysSetName, key).Result() + // isMember, err := cacheBackend.rdb.SIsMember(context.Background(), cacheBackend.keysSetName, key).Result() + isMember, err := pipe.SIsMember(context.Background(), cacheBackend.keysSetName, key).Result() if err != nil { return nil, false } @@ -83,8 +88,13 @@ func (cacheBackend *RedisBackend) Get(key string) (item *models.Item, ok bool) { // Get the item from the cacheBackend item = models.ItemPool.Get().(*models.Item) - data, err := cacheBackend.rdb.HGet(context.Background(), key, "data").Bytes() + // data, err := cacheBackend.rdb.HGet(context.Background(), key, "data").Bytes() + data, err := pipe.HGet(context.Background(), key, "data").Bytes() + pipe.Exec(context.Background()) if err != nil { + // Return the item to the pool + models.ItemPool.Put(item) + // Check if the item is not found if err == redis.Nil { return nil, false @@ -92,7 +102,7 @@ func (cacheBackend *RedisBackend) Get(key string) (item *models.Item, ok bool) { return nil, false } // Deserialize the item - err = cacheBackend.Serializer.Deserialize(data, item) + err = cacheBackend.Serializer.Unmarshal(data, item) if err != nil { return nil, false } @@ -101,21 +111,27 @@ func (cacheBackend *RedisBackend) Get(key string) (item *models.Item, ok bool) { // Set stores the Item in the cacheBackend. func (cacheBackend *RedisBackend) Set(item *models.Item) error { + pipe := cacheBackend.rdb.TxPipeline() + // Check if the item is valid if err := item.Valid(); err != nil { + // Return the item to the pool + models.ItemPool.Put(item) return err } // Serialize the item - data, err := cacheBackend.Serializer.Serialize(item) + data, err := cacheBackend.Serializer.Marshal(item) if err != nil { + // Return the item to the pool + models.ItemPool.Put(item) return err } expiration := item.Expiration.String() // Store the item in the cacheBackend - err = cacheBackend.rdb.HSet(context.Background(), item.Key, map[string]interface{}{ + err = pipe.HSet(context.Background(), item.Key, map[string]interface{}{ "data": data, "expiration": expiration, }).Err() @@ -123,15 +139,15 @@ func (cacheBackend *RedisBackend) Set(item *models.Item) error { if err != nil { return err } - // Add the key to the set of keys associated with the cache prefix - cacheBackend.rdb.SAdd(context.Background(), cacheBackend.keysSetName, item.Key) - + pipe.SAdd(context.Background(), cacheBackend.keysSetName, item.Key) // Set the expiration if it is not zero if item.Expiration > 0 { - cacheBackend.rdb.Expire(context.Background(), item.Key, item.Expiration) + pipe.Expire(context.Background(), item.Key, item.Expiration) } + pipe.Exec(context.Background()) + return nil } @@ -140,111 +156,76 @@ func (cacheBackend *RedisBackend) List(options ...FilterOption[RedisBackend]) ([ // Apply the filter options ApplyFilterOptions(cacheBackend, options...) - // Get the set of keys + // Get the set of keys held in the cacheBackend with the given `keysSetName` keys, err := cacheBackend.rdb.SMembers(context.Background(), cacheBackend.keysSetName).Result() if err != nil { return nil, err } + // Execute the Redis commands in a pipeline transaction to improve performance and reduce the number of round trips + cmds, err := cacheBackend.rdb.Pipelined(context.Background(), func(pipe redis.Pipeliner) error { + // Get the items from the cacheBackend + for _, key := range keys { + pipe.HGet(context.Background(), key, "data") + } + return nil + }) + if err != nil { + return nil, err + } + // Create a slice to hold the items items := make([]*models.Item, 0, len(keys)) - // Iterate over the keys and retrieve the items - for _, key := range keys { - // Get the item from the cacheBackend - item, ok := cacheBackend.Get(key) - if ok { - // Check if the item matches the filter options + // Deserialize the items and add them to the slice of items to return + for _, cmd := range cmds { + data, _ := cmd.(*redis.StringCmd).Bytes() // Ignore the error because it is already checked in the pipeline transaction + item := models.ItemPool.Get().(*models.Item) + err := cacheBackend.Serializer.Unmarshal(data, item) + if err == nil { if cacheBackend.FilterFunc != nil && !cacheBackend.FilterFunc(item) { continue } items = append(items, item) + } else { + // Return the item to the pool + models.ItemPool.Put(item) } } - // Sort the items + // Check if the items should be sorted if cacheBackend.SortBy == "" { // No sorting return items, nil } + // Sort the items var sorter sort.Interface switch cacheBackend.SortBy { - case types.SortByKey.String(): + case types.SortByKey.String(): // Sort by key sorter = &itemSorterByKey{items: items} - case types.SortByValue.String(): + case types.SortByValue.String(): // Sort by value: it can cause problems if the value is not a string sorter = &itemSorterByValue{items: items} - case types.SortByLastAccess.String(): + case types.SortByLastAccess.String(): // Sort by last access sorter = &itemSorterByLastAccess{items: items} - case types.SortByAccessCount.String(): + case types.SortByAccessCount.String(): // Sort by access count sorter = &itemSorterByAccessCount{items: items} - case types.SortByExpiration.String(): + case types.SortByExpiration.String(): // Sort by expiration sorter = &itemSorterByExpiration{items: items} default: return nil, fmt.Errorf("unknown sortBy field: %s", cacheBackend.SortBy) } + // Reverse the sort order if needed if !cacheBackend.SortAscending { sorter = sort.Reverse(sorter) } - + // Sort the items by the given field sort.Sort(sorter) return items, nil } -// func (cacheBackend *RedisBackend) List(options ...FilterOption[RedisBackend]) ([]*models.Item, error) { -// // Apply the filter options -// // Apply the filter options -// ApplyFilterOptions(cacheBackend, options...) - -// // Initialize the score range -// var min, max string -// if cacheBackend.SortAscending { -// min = "-inf" -// max = "+inf" -// } else { -// min = "+inf" -// max = "-inf" -// } - -// // Get the keys from the sorted set -// var keys []string -// var err error -// zrangeby := &redis.ZRangeBy{ -// Min: min, Max: max, -// } -// if cacheBackend.SortBy == types.SortByLastAccess.String() { -// keys, err = cacheBackend.rdb.ZRangeByScore(context.Background(), "last_access_sort", zrangeby).Result() -// } else if cacheBackend.SortBy == types.SortByAccessCount.String() { -// keys, err = cacheBackend.rdb.ZRangeByScore(context.Background(), "access_count_sort", zrangeby).Result() -// } else { -// return nil, fmt.Errorf("invalid sorting criteria: %s", cacheBackend.SortBy) -// } -// if err != nil { -// return nil, err -// } - -// // Create a slice to hold the items -// items := make([]*models.Item, 0, len(keys)) - -// // Iterate over the keys and retrieve the items -// for _, key := range keys { -// // Get the item from the cacheBackend -// item, ok := cacheBackend.Get(key) -// if !ok { -// continue -// } -// // Check if the item matches the filter options -// if cacheBackend.FilterFunc != nil && !cacheBackend.FilterFunc(item) { -// continue -// } -// items = append(items, item) -// } - -// return items, nil -// } - // Remove removes an item from the cache with the given key func (cacheBackend *RedisBackend) Remove(keys ...string) error { _, err := cacheBackend.rdb.Del(context.Background(), keys...).Result() diff --git a/backend/redis/options.go b/backend/redis/options.go index 8a21216..e5b5046 100644 --- a/backend/redis/options.go +++ b/backend/redis/options.go @@ -4,73 +4,106 @@ import ( "crypto/tls" "time" - "github.com/go-redis/redis/v8" + "github.com/go-redis/redis/v9" ) +// Option is a function type that can be used to configure the `RedisBackend`. type Option func(*redis.Options) +// ApplyOptions applies the given options to the given backend. func ApplyOptions(opt *redis.Options, options ...Option) { for _, option := range options { option(opt) } } +// WithAddr sets the `Addr` field of the `redis.Options` struct. func WithAddr(addr string) Option { return func(opt *redis.Options) { opt.Addr = addr } } +// WithUsername sets the `Username` field of the `redis.Options` struct. +func WithUsername(username string) Option { + return func(opt *redis.Options) { + opt.Username = username + } +} + +// WithPassword sets the `Password` field of the `redis.Options` struct. func WithPassword(password string) Option { return func(opt *redis.Options) { opt.Password = password } } +// WithDB sets the `DB` field of the `redis.Options` struct. func WithDB(db int) Option { return func(opt *redis.Options) { opt.DB = db } } +// WithMaxRetries sets the `MaxRetries` field of the `redis.Options` struct. func WithMaxRetries(maxRetries int) Option { return func(opt *redis.Options) { opt.MaxRetries = maxRetries } } +// WithDialTimeout sets the `DialTimeout` field of the `redis.Options` struct. func WithDialTimeout(dialTimeout time.Duration) Option { return func(opt *redis.Options) { opt.DialTimeout = dialTimeout } } +// WithReadTimeout sets the `ReadTimeout` field of the `redis.Options` struct. func WithReadTimeout(readTimeout time.Duration) Option { return func(opt *redis.Options) { opt.ReadTimeout = readTimeout } } +// WithWriteTimeout sets the `WriteTimeout` field of the `redis.Options` struct. func WithWriteTimeout(writeTimeout time.Duration) Option { return func(opt *redis.Options) { opt.WriteTimeout = writeTimeout } } +// WithPoolFIFO sets the `PoolFIFO` field of the `redis.Options` struct. func WithPoolFIFO(poolFIFO bool) Option { return func(opt *redis.Options) { opt.PoolFIFO = poolFIFO } } +// WithPoolSize sets the `PoolSize` field of the `redis.Options` struct. func WithPoolSize(poolSize int) Option { return func(opt *redis.Options) { opt.PoolSize = poolSize } } +// WithPoolTimeout sets the `PoolTimeout` field of the `redis.Options` struct. +func WithPoolTimeout(poolTimeout time.Duration) Option { + return func(opt *redis.Options) { + opt.PoolTimeout = poolTimeout + } +} + +// WithTLSConfig sets the `TLSConfig` field of the `redis.Options` struct. func WithTLSConfig(tlsConfig *tls.Config) Option { return func(opt *redis.Options) { opt.TLSConfig = tlsConfig } } + +// WithMinIdleConns sets the `MinIdleConns` field of the `redis.Options` struct. +func WithMinIdleConns(minIdleConns int) Option { + return func(opt *redis.Options) { + opt.MinIdleConns = minIdleConns + } +} diff --git a/backend/redis/store.go b/backend/redis/store.go index 9722f04..06ed585 100644 --- a/backend/redis/store.go +++ b/backend/redis/store.go @@ -1,38 +1,42 @@ package redis import ( + "context" "fmt" + "net" + "strings" "time" - "github.com/go-redis/redis/v8" + "github.com/go-redis/redis/v9" ) +// Store is a redis store instance with redis client type Store struct { Client *redis.Client } -// New -// @param opt -func New(opt *redis.Options) *Store { - cli := redis.NewClient(opt) - - return &Store{Client: cli} -} - -func NewStore(opts ...Option) (*Store, error) { +// New create redis store instance with given options and config +// @param opts +func New(opts ...Option) (*Store, error) { // Setup redis client opt := &redis.Options{ + Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) { + return net.DialTimeout(network, addr, 10*time.Second) + }, + DB: 0, MaxRetries: 10, DialTimeout: 10 * time.Second, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, PoolFIFO: false, - PoolSize: 10, + PoolSize: 20, + MinIdleConns: 10, + PoolTimeout: 30 * time.Second, } ApplyOptions(opt, opts...) - if opt.Addr == "" { + if strings.TrimSpace(opt.Addr) == "" { return nil, fmt.Errorf("redis address is empty") } @@ -40,57 +44,3 @@ func NewStore(opts ...Option) (*Store, error) { return &Store{Client: cli}, nil } - -// NewWithDb -// @param tx -func NewWithDb(tx *redis.Client) *Store { - return &Store{Client: tx} -} - -// func NewOptions() (opt *redis.Options, err error) { - -// // read options from config -// opt = &redis.Options{ -// Addr: config.Redis.Address, -// Password: config.Redis.Password, -// DB: config.Redis.DB, -// MaxRetries: 10, -// DialTimeout: 10 * time.Second, -// ReadTimeout: 30 * time.Second, -// WriteTimeout: 30 * time.Second, -// PoolFIFO: false, -// PoolSize: 10, -// } - -// // read CA cert -// caPath, ok := os.LookupEnv("TLS_CA_CERT_FILE") -// if ok && caPath != "" { -// // ref https://pkg.go.dev/crypto/tls#example-Dial -// rootCertPool := x509.NewCertPool() -// pem, err := ioutil.ReadFile(caPath) -// if err != nil { -// return nil, err -// } -// if ok := rootCertPool.AppendCertsFromPEM(pem); !ok { -// return nil, fmt.Errorf("failed to append root CA cert at %s", caPath) -// } -// opt.TLSConfig = &tls.Config{ -// RootCAs: rootCertPool, -// } - -// // https://pkg.go.dev/crypto/tls#LoadX509KeyPair -// clientCert, ok := os.LookupEnv("TLS_CERT_FILE") -// clientKey, ok2 := os.LookupEnv("TLS_KEY_FILE") -// if ok && ok2 { -// cert, err := tls.LoadX509KeyPair(clientCert, clientKey) -// if err != nil { -// return nil, err -// } -// opt.TLSConfig.Certificates = []tls.Certificate{cert} -// opt.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert -// opt.TLSConfig.ClientCAs = rootCertPool -// } -// } - -// return opt, nil -// } diff --git a/errors/errors.go b/errors/errors.go index d476f47..b8edcdd 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -36,4 +36,7 @@ var ( // ErrParamCannotBeEmpty is returned when a parameter cannot be empty. ErrParamCannotBeEmpty = errors.New("param cannot be empty") + + // ErrSerializerNotFound is returned when a serializer is not found. + ErrSerializerNotFound = errors.New("serializer not found") ) diff --git a/eviction/eviction.go b/eviction/eviction.go index 8e16201..0a7dcb6 100644 --- a/eviction/eviction.go +++ b/eviction/eviction.go @@ -6,8 +6,8 @@ import ( "github.com/hyp3rd/hypercache/errors" ) -// Algorithm is the interface that must be implemented by eviction algorithms. -type Algorithm interface { +// IAlgorithm is the interface that must be implemented by eviction algorithms. +type IAlgorithm interface { // Evict returns the next item to be evicted from the cache. Evict() (string, bool) // Set adds a new item to the cache with the given key. @@ -18,12 +18,12 @@ type Algorithm interface { Delete(key string) } -var algorithmRegistry = make(map[string]func(capacity int) (Algorithm, error)) +var algorithmRegistry = make(map[string]func(capacity int) (IAlgorithm, error)) // NewEvictionAlgorithm creates a new eviction algorithm with the given capacity. // If the capacity is negative, it returns an error. // The algorithmName parameter is used to select the eviction algorithm from the registry. -func NewEvictionAlgorithm(algorithmName string, capacity int) (Algorithm, error) { +func NewEvictionAlgorithm(algorithmName string, capacity int) (IAlgorithm, error) { // Check the parameters. if algorithmName == "" { return nil, fmt.Errorf("%s: %s", errors.ErrParamCannotBeEmpty, "algorithmName") @@ -41,25 +41,25 @@ func NewEvictionAlgorithm(algorithmName string, capacity int) (Algorithm, error) } // RegisterEvictionAlgorithm registers a new eviction algorithm with the given name. -func RegisterEvictionAlgorithm(name string, createFunc func(capacity int) (Algorithm, error)) { +func RegisterEvictionAlgorithm(name string, createFunc func(capacity int) (IAlgorithm, error)) { algorithmRegistry[name] = createFunc } // Register the default eviction algorithms. func init() { - RegisterEvictionAlgorithm("arc", func(capacity int) (Algorithm, error) { + RegisterEvictionAlgorithm("arc", func(capacity int) (IAlgorithm, error) { return NewARC(capacity) }) - RegisterEvictionAlgorithm("lru", func(capacity int) (Algorithm, error) { + RegisterEvictionAlgorithm("lru", func(capacity int) (IAlgorithm, error) { return NewLRU(capacity) }) - RegisterEvictionAlgorithm("clock", func(capacity int) (Algorithm, error) { + RegisterEvictionAlgorithm("clock", func(capacity int) (IAlgorithm, error) { return NewClockAlgorithm(capacity) }) - RegisterEvictionAlgorithm("lfu", func(capacity int) (Algorithm, error) { + RegisterEvictionAlgorithm("lfu", func(capacity int) (IAlgorithm, error) { return NewLFUAlgorithm(capacity) }) - RegisterEvictionAlgorithm("cawolfu", func(capacity int) (Algorithm, error) { + RegisterEvictionAlgorithm("cawolfu", func(capacity int) (IAlgorithm, error) { return NewCAWOLFU(capacity) }) } diff --git a/examples/redis/redis.go b/examples/redis/redis.go index 85da598..2336fa5 100644 --- a/examples/redis/redis.go +++ b/examples/redis/redis.go @@ -12,7 +12,7 @@ import ( ) func main() { - redisStore, err := redis.NewStore( + redisStore, err := redis.New( redis.WithAddr("localhost:6379"), redis.WithPassword("k7oMs2G5bc4mRN45jPZjLBZxuMFrCLahvPn648Zwq1lT41gSYZqapBRnSF2L995FaYcZBz8c7xkKXku94HeReDgdwBu1N4CzgfQ94Z504hjfzrST1u0idVkbXe8ust"), redis.WithDB(0), @@ -40,17 +40,11 @@ func main() { } } - // value, ok := hyperCache.Get("key-100") - - // if !ok { - // fmt.Println("key not found") - // } - allItems, err := hyperCache.List( backend.WithSortBy[backend.RedisBackend](types.SortByKey), backend.WithSortOrderAsc[backend.RedisBackend](true), backend.WithFilterFunc[backend.RedisBackend](func(item *models.Item) bool { - return item.Value == "value-210" + return item.Value != "value-210" }), ) diff --git a/go.mod b/go.mod index fd0dee1..f9bf579 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,9 @@ module github.com/hyp3rd/hypercache go 1.19 require ( - github.com/go-redis/redis v6.15.9+incompatible - github.com/go-redis/redis/v8 v8.11.5 + github.com/go-redis/redis/v9 v9.0.0-rc.2 github.com/google/go-cmp v0.5.9 + github.com/kelindar/binary v1.0.17 github.com/longbridgeapp/assert v1.1.0 github.com/shamaton/msgpack/v2 v2.1.1 go.uber.org/zap v1.24.0 @@ -17,7 +17,7 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/onsi/gomega v1.24.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/testify v1.8.0 // indirect + github.com/stretchr/testify v1.8.1 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 57e707f..a1ca4f6 100644 --- a/go.sum +++ b/go.sum @@ -7,12 +7,12 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= -github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= -github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= -github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/go-redis/redis/v9 v9.0.0-rc.2 h1:IN1eI8AvJJeWHjMW/hlFAv2sAfvTun2DVksDDJ3a6a0= +github.com/go-redis/redis/v9 v9.0.0-rc.2/go.mod h1:cgBknjwcBJa2prbnuHH/4k/Mlj4r0pWNV2HBanHujfY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/kelindar/binary v1.0.17 h1:DANIwtqpi9EuD71gmiecWASpyKK6C1iCTcx0VaP5QLk= +github.com/kelindar/binary v1.0.17/go.mod h1:/twdz8gRLNMffx0U4UOgqm1LywPs6nd9YK2TX52MDh8= github.com/longbridgeapp/assert v1.1.0 h1:L+/HISOhuGbNAAmJNXgk3+Tm5QmSB70kwdktJXgjL+I= github.com/longbridgeapp/assert v1.1.0/go.mod h1:UOI7O3rzlzlz715lQm0atWs6JbrYGuIJUEeOekutL6o= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -26,10 +26,12 @@ github.com/shamaton/msgpack/v2 v2.1.1 h1:gAMxOtVJz93R0EwewwUc8tx30n34aV6BzJuwHE8 github.com/shamaton/msgpack/v2 v2.1.1/go.mod h1:aTUEmh31ziGX1Ml7wMPLVY0f4vT3CRsCvZRoSCs+VGg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= diff --git a/hypercache.go b/hypercache.go index 7ffaf94..2fc083e 100644 --- a/hypercache.go +++ b/hypercache.go @@ -41,7 +41,7 @@ type HyperCache[T backend.IBackendConstrain] struct { expirationTriggerCh chan bool // `expirationTriggerCh` channel to signal the expiration trigger loop to start evictCh chan bool // `evictCh` channel to signal the eviction loop to start evictionAlgorithmName string // `evictionAlgorithmName` name of the eviction algorithm to use when evicting items - evictionAlgorithm eviction.Algorithm // `evictionAlgorithm` eviction algorithm to use when evicting items + evictionAlgorithm eviction.IAlgorithm // `evictionAlgorithm` eviction algorithm to use when evicting items expirationInterval time.Duration // `expirationInterval` interval at which the expiration loop should run evictionInterval time.Duration // interval at which the eviction loop should run maxEvictionCount uint // `evictionInterval` maximum number of items that can be evicted in a single eviction loop iteration @@ -49,7 +49,7 @@ type HyperCache[T backend.IBackendConstrain] struct { once sync.Once // `once` holds a Once struct that is used to ensure that the expiration and eviction loops are only started once statsCollectorName string // `statsCollectorName` holds the name of the stats collector that the cache should use when collecting cache statistics // StatsCollector to collect cache statistics - StatsCollector stats.Collector + StatsCollector stats.ICollector } // NewInMemoryWithDefaults initializes a new HyperCache with the default configuration. diff --git a/libs/serializer/json.go b/libs/serializer/json.go index 274c8f1..1d416f8 100644 --- a/libs/serializer/json.go +++ b/libs/serializer/json.go @@ -2,18 +2,19 @@ package serializer import "encoding/json" +// DefaultJSONSerializer leverages the default `json` to serialize the items before storing them in the cache type DefaultJSONSerializer struct { } -// Serialize +// Marshal serializes the given value into a byte slice. // @param v -func (d *DefaultJSONSerializer) Serialize(v any) ([]byte, error) { - return json.Marshal(v) +func (d *DefaultJSONSerializer) Marshal(v any) ([]byte, error) { + return json.Marshal(&v) } -// Deserialize +// Unmarshal deserializes the given byte slice into the given value. // @param data // @param v -func (d *DefaultJSONSerializer) Deserialize(data []byte, v any) error { - return json.Unmarshal(data, v) +func (d *DefaultJSONSerializer) Unmarshal(data []byte, v any) error { + return json.Unmarshal(data, &v) } diff --git a/libs/serializer/msgpack.go b/libs/serializer/msgpack.go new file mode 100644 index 0000000..2ee0049 --- /dev/null +++ b/libs/serializer/msgpack.go @@ -0,0 +1,22 @@ +package serializer + +import ( + "github.com/shamaton/msgpack/v2" +) + +// MsgpackSerializer leverages `msgpack` to serialize the items before storing them in the cache +type MsgpackSerializer struct { +} + +// Marshal serializes the given value into a byte slice. +// @param v +func (d *MsgpackSerializer) Marshal(v any) ([]byte, error) { + return msgpack.Marshal(&v) +} + +// Unmarshal deserializes the given byte slice into the given value. +// @param data +// @param v +func (d *MsgpackSerializer) Unmarshal(data []byte, v any) error { + return msgpack.Unmarshal(data, v) +} diff --git a/libs/serializer/serializer.go b/libs/serializer/serializer.go index 56f9ea1..66813c8 100644 --- a/libs/serializer/serializer.go +++ b/libs/serializer/serializer.go @@ -1,10 +1,48 @@ package serializer +import ( + "fmt" + + "github.com/hyp3rd/hypercache/errors" +) + +// ISerializer is the interface that wraps the basic serializer methods. type ISerializer interface { - Serialize(v any) ([]byte, error) - Deserialize(data []byte, v any) error + // Marshal serializes the given value into a byte slice. + Marshal(v any) ([]byte, error) + // Unmarshal deserializes the given byte slice into the given value. + Unmarshal(data []byte, v any) error +} + +var serializerRegistry = make(map[string]func() ISerializer) + +// New returns a new serializer based on the serializerType. +func New(serializerType string) (ISerializer, error) { + if serializerType == "" { + return nil, fmt.Errorf("%s: %s", errors.ErrParamCannotBeEmpty, "serializerType") + } + + createFunc, ok := serializerRegistry[serializerType] + if !ok { + return nil, fmt.Errorf("%s: %s", errors.ErrSerializerNotFound, serializerType) + } + + return createFunc(), nil } -func New() ISerializer { - return &DefaultJSONSerializer{} +// RegisterSerializer registers a new serializer with the given name. +func RegisterSerializer(serializerType string, createFunc func() ISerializer) { + serializerRegistry[serializerType] = createFunc +} + +func init() { + // Register the default serializer. + RegisterSerializer("default", func() ISerializer { + return &DefaultJSONSerializer{} + }) + + // Register the msgpack serializer. + RegisterSerializer("msgpack", func() ISerializer { + return &MsgpackSerializer{} + }) } diff --git a/middleware/stats.go b/middleware/stats.go index 02b0c2d..f6334a3 100644 --- a/middleware/stats.go +++ b/middleware/stats.go @@ -12,11 +12,11 @@ import ( // Must implement the hypercache.Service interface. type StatsCollectorMiddleware struct { next hypercache.Service - statsCollector stats.Collector + statsCollector stats.ICollector } // NewStatsCollectorMiddleware returns a new StatsCollectorMiddleware -func NewStatsCollectorMiddleware(next hypercache.Service, statsCollector stats.Collector) hypercache.Service { +func NewStatsCollectorMiddleware(next hypercache.Service, statsCollector stats.ICollector) hypercache.Service { return &StatsCollectorMiddleware{next: next, statsCollector: statsCollector} } diff --git a/models/item.go b/models/item.go index ad287c4..b86538c 100644 --- a/models/item.go +++ b/models/item.go @@ -3,7 +3,6 @@ package models // Item represents an item in the cache. It has a key, value, expiration duration, and a last access time field. import ( - "reflect" "strings" "sync" "sync/atomic" @@ -31,25 +30,25 @@ var ItemPool = sync.Pool{ // FieldByName returns the value of the field of the Item struct with the given name. // If the field does not exist, an empty reflect.Value is returned. -func (item *Item) FieldByName(name string) reflect.Value { - // Get the reflect.Value of the item pointer - v := reflect.ValueOf(item) +// func (item *Item) FieldByName(name string) reflect.Value { +// // Get the reflect.Value of the item pointer +// v := reflect.ValueOf(item) - // Get the reflect.Value of the item struct itself by calling Elem() on the pointer value - f := v.Elem().FieldByName(name) +// // Get the reflect.Value of the item struct itself by calling Elem() on the pointer value +// f := v.Elem().FieldByName(name) - // If the field does not exist, return an empty reflect.Value - if !f.IsValid() { - return reflect.Value{} - } - // Return the field value - return f -} +// // If the field does not exist, return an empty reflect.Value +// if !f.IsValid() { +// return reflect.Value{} +// } +// // Return the field value +// return f +// } // Valid returns an error if the item is invalid, nil otherwise. func (item *Item) Valid() error { // Check for empty key - if item.Key == "" || strings.TrimSpace(item.Key) == "" { + if strings.TrimSpace(item.Key) == "" { return errors.ErrInvalidKey } diff --git a/stats/collector.go b/stats/collector.go index 4e6a881..2758974 100644 --- a/stats/collector.go +++ b/stats/collector.go @@ -7,8 +7,8 @@ import ( "github.com/hyp3rd/hypercache/types" ) -// Collector is an interface that defines the methods that a stats collector should implement. -type Collector interface { +// ICollector is an interface that defines the methods that a stats collector should implement. +type ICollector interface { // Incr increments the count of a statistic by the given value. Incr(stat types.Stat, value int64) // Decr decrements the count of a statistic by the given value. @@ -24,11 +24,11 @@ type Collector interface { } // StatsCollectorRegistry holds the a registry of stats collectors. -var StatsCollectorRegistry = make(map[string]func() (Collector, error)) +var StatsCollectorRegistry = make(map[string]func() (ICollector, error)) // NewCollector creates a new stats collector. // The statsCollectorName parameter is used to select the stats collector from the registry. -func NewCollector(statsCollectorName string) (Collector, error) { +func NewCollector(statsCollectorName string) (ICollector, error) { // Check the parameters. if statsCollectorName == "" { return nil, fmt.Errorf("%s: %s", errors.ErrParamCannotBeEmpty, "statsCollectorName") @@ -43,13 +43,13 @@ func NewCollector(statsCollectorName string) (Collector, error) { } // RegisterCollector registers a new stats collector with the given name. -func RegisterCollector(name string, createFunc func() (Collector, error)) { +func RegisterCollector(name string, createFunc func() (ICollector, error)) { StatsCollectorRegistry[name] = createFunc } func init() { // Register the default stats collector. - RegisterCollector("default", func() (Collector, error) { + RegisterCollector("default", func() (ICollector, error) { var err error collector := NewHistogramStatsCollector() if collector == nil { From 472c8cc278f9d1046f5b5c96e61cf87d6524f0d0 Mon Sep 17 00:00:00 2001 From: Francesco Cosentino Date: Mon, 16 Jan 2023 19:56:43 +0100 Subject: [PATCH 5/5] removed unnecessary deps --- go.mod | 1 - go.sum | 2 -- 2 files changed, 3 deletions(-) diff --git a/go.mod b/go.mod index f9bf579..06920b2 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.19 require ( github.com/go-redis/redis/v9 v9.0.0-rc.2 github.com/google/go-cmp v0.5.9 - github.com/kelindar/binary v1.0.17 github.com/longbridgeapp/assert v1.1.0 github.com/shamaton/msgpack/v2 v2.1.1 go.uber.org/zap v1.24.0 diff --git a/go.sum b/go.sum index a1ca4f6..ae343e0 100644 --- a/go.sum +++ b/go.sum @@ -11,8 +11,6 @@ github.com/go-redis/redis/v9 v9.0.0-rc.2 h1:IN1eI8AvJJeWHjMW/hlFAv2sAfvTun2DVksD github.com/go-redis/redis/v9 v9.0.0-rc.2/go.mod h1:cgBknjwcBJa2prbnuHH/4k/Mlj4r0pWNV2HBanHujfY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/kelindar/binary v1.0.17 h1:DANIwtqpi9EuD71gmiecWASpyKK6C1iCTcx0VaP5QLk= -github.com/kelindar/binary v1.0.17/go.mod h1:/twdz8gRLNMffx0U4UOgqm1LywPs6nd9YK2TX52MDh8= github.com/longbridgeapp/assert v1.1.0 h1:L+/HISOhuGbNAAmJNXgk3+Tm5QmSB70kwdktJXgjL+I= github.com/longbridgeapp/assert v1.1.0/go.mod h1:UOI7O3rzlzlz715lQm0atWs6JbrYGuIJUEeOekutL6o= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=