Skip to content

Commit

Permalink
Merge pull request #19 from hyp3rd/feat/multi-backend-support
Browse files Browse the repository at this point in the history
Feat/multi backend support
  • Loading branch information
hyp3rd authored Jan 20, 2023
2 parents 283ec1e + cd61972 commit bc3e3cc
Show file tree
Hide file tree
Showing 17 changed files with 193 additions and 118 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ bench:

# run runs the example specified in the example variable with the optional arguments specified in the ARGS variable.
run:
go run examples/$(example)/$(example).go $(ARGS)
go run examples/$(example)/*.go $(ARGS)

# vet runs the Go vet static analysis tool on all packages in the project.
vet:
Expand Down
8 changes: 4 additions & 4 deletions backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (

// IBackendConstrain is the interface that defines the constrain type that must be implemented by cache backends.
type IBackendConstrain interface {
InMemory | RedisBackend
InMemory | Redis
}

// IInMemory is the interface that must be implemented by in-memory cache backends.
Expand All @@ -25,7 +25,7 @@ type IRedisBackend[T IBackendConstrain] interface {
// IBackend[T] is the interface that must be implemented by cache backends.
IBackend[T]
// List the items in the cache that meet the specified criteria.
List(options ...FilterOption[RedisBackend]) ([]*models.Item, error)
List(options ...FilterOption[Redis]) ([]*models.Item, error)
// Clear removes all items from the cache.
Clear() error
}
Expand Down Expand Up @@ -58,9 +58,9 @@ func NewBackend[T IBackendConstrain](backendType string, opts ...any) (IBackend[
}
return NewInMemory(Options...)
case "redis":
Options := make([]Option[RedisBackend], len(opts))
Options := make([]Option[Redis], len(opts))
for i, option := range opts {
Options[i] = option.(Option[RedisBackend])
Options[i] = option.(Option[Redis])
}
return NewRedisBackend(Options...)
default:
Expand Down
55 changes: 33 additions & 22 deletions backend/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ func (inm *InMemory) setSortAscending(ascending bool) {
inm.SortAscending = ascending
}

// setSortAscending sets the `SortAscending` field of the `RedisBackend` backend.
func (rb *RedisBackend) setSortAscending(ascending bool) {
// setSortAscending sets the `SortAscending` field of the `Redis` backend.
func (rb *Redis) setSortAscending(ascending bool) {
rb.SortAscending = ascending
}

Expand All @@ -30,8 +30,8 @@ func (inm *InMemory) setSortBy(sortBy string) {
inm.SortBy = sortBy
}

// setSortBy sets the `SortBy` field of the `RedisBackend` backend.
func (rb *RedisBackend) setSortBy(sortBy string) {
// setSortBy sets the `SortBy` field of the `Redis` backend.
func (rb *Redis) setSortBy(sortBy string) {
rb.SortBy = sortBy
}

Expand All @@ -48,11 +48,27 @@ func (inm *InMemory) setFilterFunc(filterFunc FilterFunc) {
inm.FilterFunc = filterFunc
}

// setFilterFunc sets the `FilterFunc` field of the `RedisBackend` backend.
func (rb *RedisBackend) setFilterFunc(filterFunc FilterFunc) {
// setFilterFunc sets the `FilterFunc` field of the `Redis` backend.
func (rb *Redis) setFilterFunc(filterFunc FilterFunc) {
rb.FilterFunc = filterFunc
}

// iConfigurableBackend is an interface that defines the methods that a backend should implement to be configurable.
type iConfigurableBackend interface {
// setCapacity sets the capacity of the cache.
setCapacity(capacity int)
}

// setCapacity sets the `Capacity` field of the `InMemory` backend.
func (inm *InMemory) setCapacity(capacity int) {
inm.capacity = capacity
}

// setCapacity sets the `Capacity` field of the `Redis` backend.
func (rb *Redis) setCapacity(capacity int) {
rb.capacity = capacity
}

// Option is a function type that can be used to configure the `HyperCache` struct.
type Option[T IBackendConstrain] func(*T)

Expand All @@ -64,22 +80,24 @@ func ApplyOptions[T IBackendConstrain](backend *T, options ...Option[T]) {
}

// WithCapacity is an option that sets the capacity of the cache.
func WithCapacity[T InMemory](capacity int) Option[InMemory] {
return func(backend *InMemory) {
backend.capacity = capacity
func WithCapacity[T IBackendConstrain](capacity int) Option[T] {
return func(a *T) {
if configurable, ok := any(a).(iConfigurableBackend); ok {
configurable.setCapacity(capacity)
}
}
}

// WithRedisClient is an option that sets the redis client to use.
func WithRedisClient[T RedisBackend](client *redis.Client) Option[RedisBackend] {
return func(backend *RedisBackend) {
func WithRedisClient[T Redis](client *redis.Client) Option[Redis] {
return func(backend *Redis) {
backend.rdb = client
}
}

// 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) {
func WithKeysSetName[T Redis](keysSetName string) Option[Redis] {
return func(backend *Redis) {
backend.keysSetName = keysSetName
}
}
Expand All @@ -88,8 +106,8 @@ func WithKeysSetName[T RedisBackend](keysSetName string) Option[RedisBackend] {
// - 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) {
func WithSerializer[T Redis](serializer serializer.ISerializer) Option[Redis] {
return func(backend *Redis) {
backend.Serializer = serializer
}
}
Expand Down Expand Up @@ -132,11 +150,4 @@ func WithFilterFunc[T any](fn func(item *models.Item) bool) FilterOption[T] {
filterable.setFilterFunc(fn)
}
}

// return func(a *T) {
// switch filter := any(a).(type) {
// case *InMemory:
// filter.FilterFunc = fn
// }
// }
}
77 changes: 42 additions & 35 deletions backend/redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,73 +12,79 @@ import (
"github.com/hyp3rd/hypercache/types"
)

// RedisBackend is a cache backend that stores the items in a redis implementation.
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
keysSetName string // keysSetName is the name of the set that holds the keys of the items in the cache
// Redis is a cache backend that stores the items in a redis implementation.
type Redis 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
keysSetName string // keysSetName is the name of the set that holds the keys of the items in the cache
// mutex sync.RWMutex // mutex to protect the cache from concurrent access
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.
func NewRedisBackend[T RedisBackend](redisOptions ...Option[RedisBackend]) (backend IRedisBackend[T], err error) {
rb := &RedisBackend{}
func NewRedisBackend[T Redis](redisOptions ...Option[Redis]) (backend IRedisBackend[T], err error) {
rb := &Redis{}
// Apply the backend options
ApplyOptions(rb, redisOptions...)

// Check if the client is nil
if rb.rdb == nil {
return nil, errors.ErrNilClient
}
// Check if the capacity is valid
// Check if the `capacity` is valid
if rb.capacity < 0 {
return nil, errors.ErrInvalidCapacity
}

// Check if the `keysSetName` is empty
if rb.keysSetName == "" {
rb.keysSetName = "hypercache"
}

rb.Serializer, err = serializer.New("msgpack")
if err != nil {
return nil, err
// Check if the serializer is nil
if rb.Serializer == nil {
// Set a the serializer to default to `msgpack`
rb.Serializer, err = serializer.New("msgpack")
if err != nil {
return nil, err
}
}

// return the new backend
return rb, nil
}

// Capacity returns the maximum number of items that can be stored in the cache.
func (cacheBackend *RedisBackend) Capacity() int {
func (cacheBackend *Redis) Capacity() int {
return cacheBackend.capacity
}

// SetCapacity sets the capacity of the cache.
func (cacheBackend *RedisBackend) SetCapacity(capacity int) {
func (cacheBackend *Redis) SetCapacity(capacity int) {
if capacity < 0 {
return
}
cacheBackend.capacity = capacity
}

// itemCount returns the number of items in the cache.
func (cacheBackend *RedisBackend) itemCount() int {
func (cacheBackend *Redis) itemCount() int {
count, _ := cacheBackend.rdb.DBSize(context.Background()).Result()
return int(count)
}

// Size returns the number of items in the cache.
func (cacheBackend *RedisBackend) Size() int {
func (cacheBackend *Redis) Size() int {
return cacheBackend.itemCount()
}

// 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()
func (cacheBackend *Redis) Get(key string) (item *models.Item, ok bool) {
// pipe := cacheBackend.rdb.Conn().Pipeline()
// Check if the key is in the set of keys
// isMember, err := cacheBackend.rdb.SIsMember(context.Background(), cacheBackend.keysSetName, key).Result()
isMember, err := pipe.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
}
Expand All @@ -88,13 +94,12 @@ 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 := pipe.HGet(context.Background(), key, "data").Bytes()
pipe.Exec(context.Background())
// Return the item to the pool
defer models.ItemPool.Put(item)
data, err := cacheBackend.rdb.HGet(context.Background(), key, "data").Bytes()
// data, _ := pipe.HGet(context.Background(), key, "data").Bytes()
// _, err = 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
Expand All @@ -110,21 +115,19 @@ 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 {
func (cacheBackend *Redis) 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.Marshal(item)
if err != nil {
// Return the item to the pool
models.ItemPool.Put(item)
return err
}

Expand Down Expand Up @@ -152,7 +155,7 @@ func (cacheBackend *RedisBackend) Set(item *models.Item) error {
}

// List returns a list of all the items in the cacheBackend that match the given filter options.
func (cacheBackend *RedisBackend) List(options ...FilterOption[RedisBackend]) ([]*models.Item, error) {
func (cacheBackend *Redis) List(options ...FilterOption[Redis]) ([]*models.Item, error) {
// Apply the filter options
ApplyFilterOptions(cacheBackend, options...)

Expand Down Expand Up @@ -181,15 +184,14 @@ func (cacheBackend *RedisBackend) List(options ...FilterOption[RedisBackend]) ([
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)
// Return the item to the pool
defer models.ItemPool.Put(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)
}
}

Expand Down Expand Up @@ -227,13 +229,18 @@ func (cacheBackend *RedisBackend) List(options ...FilterOption[RedisBackend]) ([
}

// 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()
func (cacheBackend *Redis) Remove(keys ...string) error {
pipe := cacheBackend.rdb.TxPipeline()

pipe.SRem(context.Background(), cacheBackend.keysSetName, keys).Result()
pipe.Del(context.Background(), keys...).Result()

_, err := pipe.Exec(context.Background())
return err
}

// Clear removes all items from the cache
func (cacheBackend *RedisBackend) Clear() error {
func (cacheBackend *Redis) Clear() error {
_, err := cacheBackend.rdb.FlushDB(context.Background()).Result()
return err
}
2 changes: 1 addition & 1 deletion backend/redis/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"github.com/go-redis/redis/v9"
)

// Option is a function type that can be used to configure the `RedisBackend`.
// Option is a function type that can be used to configure the `Redis`.
type Option func(*redis.Options)

// ApplyOptions applies the given options to the given backend.
Expand Down
6 changes: 3 additions & 3 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import (
type Config[T backend.IBackendConstrain] struct {
// InMemoryOptions is a slice of options that can be used to configure the `InMemory`.
InMemoryOptions []backend.Option[backend.InMemory]
// RedisOptions is a slice of options that can be used to configure the `RedisBackend`.
RedisOptions []backend.Option[backend.RedisBackend]
// RedisOptions is a slice of options that can be used to configure the `Redis`.
RedisOptions []backend.Option[backend.Redis]
// HyperCacheOptions is a slice of options that can be used to configure `HyperCache`.
HyperCacheOptions []Option[T]
}
Expand All @@ -29,7 +29,7 @@ type Config[T backend.IBackendConstrain] struct {
func NewConfig[T backend.IBackendConstrain]() *Config[T] {
return &Config[T]{
InMemoryOptions: []backend.Option[backend.InMemory]{},
RedisOptions: []backend.Option[backend.RedisBackend]{},
RedisOptions: []backend.Option[backend.Redis]{},
HyperCacheOptions: []Option[T]{
WithExpirationInterval[T](30 * time.Minute),
WithEvictionAlgorithm[T]("lfu"),
Expand Down
Loading

0 comments on commit bc3e3cc

Please sign in to comment.