From fc19710b058b26abbfd0e470cb38dfe1401cc1a2 Mon Sep 17 00:00:00 2001 From: Francesco Cosentino Date: Sat, 21 Jan 2023 18:49:50 +0100 Subject: [PATCH 1/9] added Size field to Item and implemented logic to calculate the size --- backend/inmemory.go | 28 ++- backend/options.go | 22 ++ backend/redis.go | 7 +- errors/errors.go | 9 + examples/list/list.go | 2 +- examples/size/size.go | 541 ++++++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 + hypercache.go | 21 +- models/item.go | 129 +++++----- 10 files changed, 696 insertions(+), 66 deletions(-) create mode 100644 examples/size/size.go diff --git a/backend/inmemory.go b/backend/inmemory.go index 91064f1..895e8c7 100644 --- a/backend/inmemory.go +++ b/backend/inmemory.go @@ -13,10 +13,11 @@ import ( // InMemory is a cache backend that stores the items in memory, leveraging a custom `ConcurrentMap`. type InMemory struct { - items datastructure.ConcurrentMap[string, *models.Item] // map to store the items in the cache - capacity int // capacity of the cache, limits the number of items that can be stored in the cache - mutex sync.RWMutex // mutex to protect the cache from concurrent access - SortFilters // filters applied when listing the items in the cache + items datastructure.ConcurrentMap[string, *models.Item] // map to store the items in the cache + capacity int // capacity of the cache, limits the number of items that can be stored in the cache + maxCacheSize int // maxCacheSize instructs the cache not allocate more memory than this limit, value in MB, 0 means no limit + mutex sync.RWMutex // mutex to protect the cache from concurrent access + SortFilters // filters applied when listing the items in the cache } // NewInMemory creates a new in-memory cache with the given options. @@ -32,6 +33,10 @@ func NewInMemory[T InMemory](opts ...Option[InMemory]) (backend IInMemory[T], er return nil, errors.ErrInvalidCapacity } + if InMemory.maxCacheSize < 0 { + return nil, errors.ErrInvalidMaxCacheSize + } + return InMemory, nil } @@ -71,6 +76,13 @@ func (cacheBackend *InMemory) Set(item *models.Item) error { cacheBackend.mutex.Lock() defer cacheBackend.mutex.Unlock() + // check if adding this item will exceed the maxCacheSize + currentSize := cacheBackend.maxCacheSize + newSize := currentSize + item.Size + if cacheBackend.maxCacheSize > 0 && newSize > cacheBackend.maxCacheSize { + return errors.ErrCacheFull + } + cacheBackend.items.Set(item.Key, item) return nil } @@ -138,3 +150,11 @@ func (cacheBackend *InMemory) Capacity() int { func (cacheBackend *InMemory) Size() int { return cacheBackend.itemCount() } + +func (cacheBackend *InMemory) SetMaxCacheSize(maxCacheSize int) { + cacheBackend.maxCacheSize = maxCacheSize +} + +func (cacheBackend *InMemory) MaxCacheSize() int { + return cacheBackend.maxCacheSize +} diff --git a/backend/options.go b/backend/options.go index b0ab041..25f1352 100644 --- a/backend/options.go +++ b/backend/options.go @@ -57,6 +57,8 @@ func (rb *Redis) setFilterFunc(filterFunc FilterFunc) { type iConfigurableBackend interface { // setCapacity sets the capacity of the cache. setCapacity(capacity int) + // setMaxCacheSize sets the maximum size of the cache. + setMaxCacheSize(maxCacheSize int) } // setCapacity sets the `Capacity` field of the `InMemory` backend. @@ -69,6 +71,15 @@ func (rb *Redis) setCapacity(capacity int) { rb.capacity = capacity } +// setMaxCacheSize sets the `maxCacheSize` field of the `InMemory` backend. +func (inm *InMemory) setMaxCacheSize(maxCacheSize int) { + inm.maxCacheSize = maxCacheSize +} + +func (rb *Redis) setMaxCacheSize(maxCacheSize int) { + rb.maxCacheSize = maxCacheSize +} + // Option is a function type that can be used to configure the `HyperCache` struct. type Option[T IBackendConstrain] func(*T) @@ -79,6 +90,17 @@ func ApplyOptions[T IBackendConstrain](backend *T, options ...Option[T]) { } } +// WithMaxCacheSize is an option that sets the maximum size of the cache. +// The maximum size of the cache is the maximum number of items that can be stored in the cache. +// If the maximum size of the cache is reached, the least recently used item will be evicted from the cache. +func WithMaxCacheSize[T IBackendConstrain](maxCacheSize int) Option[T] { + return func(a *T) { + if configurable, ok := any(a).(iConfigurableBackend); ok { + configurable.setMaxCacheSize(maxCacheSize) + } + } +} + // WithCapacity is an option that sets the capacity of the cache. func WithCapacity[T IBackendConstrain](capacity int) Option[T] { return func(a *T) { diff --git a/backend/redis.go b/backend/redis.go index 4cd5844..6c7776f 100644 --- a/backend/redis.go +++ b/backend/redis.go @@ -14,9 +14,10 @@ import ( // 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 + 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 + maxCacheSize int // maxCacheSize is the maximum number of items that can be stored 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 diff --git a/errors/errors.go b/errors/errors.go index b8edcdd..e8086b3 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -39,4 +39,13 @@ var ( // ErrSerializerNotFound is returned when a serializer is not found. ErrSerializerNotFound = errors.New("serializer not found") + + // ErrInvalidSize is returned when an invalid size is passed to the cache. + ErrInvalidSize = errors.New("invalid size") + + // ErrInvalidMaxCacheSize is returned when an invalid max cache size is passed to the cache. + ErrInvalidMaxCacheSize = errors.New("invalid max cache size") + + // ErrCacheFull is returned when the cache is full. + ErrCacheFull = errors.New("cache is full") ) diff --git a/examples/list/list.go b/examples/list/list.go index 9f860a4..53d4de9 100644 --- a/examples/list/list.go +++ b/examples/list/list.go @@ -40,7 +40,7 @@ func main() { 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/size/size.go b/examples/size/size.go new file mode 100644 index 0000000..b4fe019 --- /dev/null +++ b/examples/size/size.go @@ -0,0 +1,541 @@ +package main + +import ( + "fmt" + "time" + + "github.com/hyp3rd/hypercache" +) + +func main() { + hypercache, err := hypercache.NewInMemoryWithDefaults(10) + + if err != nil { + panic(err) + } + + type User struct { + Name string + Age int + LastAccess time.Time + Pass string + Email string + Phone string + IsAdmin bool + IsGuest bool + IsBanned bool + } + + users := []User{ + { + Name: "John", + Age: 30, + LastAccess: time.Now(), + Pass: "123456", + Email: "jhon@example.com", + Phone: "123456789", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John1", + Age: 31, + LastAccess: time.Now(), + Pass: "1234567", + Email: "jhon1@example.com", + Phone: "1234567891011", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John2", + Age: 32, + LastAccess: time.Now(), + Pass: "12345678", + Email: "jhon1@example.com", + Phone: "1234567891012", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John", + Age: 30, + LastAccess: time.Now(), + Pass: "123456", + Email: "jhon@example.com", + Phone: "123456789", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John1", + Age: 31, + LastAccess: time.Now(), + Pass: "1234567", + Email: "jhon1@example.com", + Phone: "1234567891011", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John2", + Age: 32, + LastAccess: time.Now(), + Pass: "12345678", + Email: "jhon1@example.com", + Phone: "1234567891012", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John", + Age: 30, + LastAccess: time.Now(), + Pass: "123456", + Email: "jhon@example.com", + Phone: "123456789", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John1", + Age: 31, + LastAccess: time.Now(), + Pass: "1234567", + Email: "jhon1@example.com", + Phone: "1234567891011", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John2", + Age: 32, + LastAccess: time.Now(), + Pass: "12345678", + Email: "jhon1@example.com", + Phone: "1234567891012", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John", + Age: 30, + LastAccess: time.Now(), + Pass: "123456", + Email: "jhon@example.com", + Phone: "123456789", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John1", + Age: 31, + LastAccess: time.Now(), + Pass: "1234567", + Email: "jhon1@example.com", + Phone: "1234567891011", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John2", + Age: 32, + LastAccess: time.Now(), + Pass: "12345678", + Email: "jhon1@example.com", + Phone: "1234567891012", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John1", + Age: 31, + LastAccess: time.Now(), + Pass: "1234567", + Email: "jhon1@example.com", + Phone: "1234567891011", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John2", + Age: 32, + LastAccess: time.Now(), + Pass: "12345678", + Email: "jhon1@example.com", + Phone: "1234567891012", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John", + Age: 30, + LastAccess: time.Now(), + Pass: "123456", + Email: "jhon@example.com", + Phone: "123456789", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John1", + Age: 31, + LastAccess: time.Now(), + Pass: "1234567", + Email: "jhon1@example.com", + Phone: "1234567891011", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John2", + Age: 32, + LastAccess: time.Now(), + Pass: "12345678", + Email: "jhon1@example.com", + Phone: "1234567891012", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John", + Age: 30, + LastAccess: time.Now(), + Pass: "123456", + Email: "jhon@example.com", + Phone: "123456789", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John1", + Age: 31, + LastAccess: time.Now(), + Pass: "1234567", + Email: "jhon1@example.com", + Phone: "1234567891011", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John2", + Age: 32, + LastAccess: time.Now(), + Pass: "12345678", + Email: "jhon1@example.com", + Phone: "1234567891012", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John", + Age: 30, + LastAccess: time.Now(), + Pass: "123456", + Email: "jhon@example.com", + Phone: "123456789", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John1", + Age: 31, + LastAccess: time.Now(), + Pass: "1234567", + Email: "jhon1@example.com", + Phone: "1234567891011", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John2", + Age: 32, + LastAccess: time.Now(), + Pass: "12345678", + Email: "jhon1@example.com", + Phone: "1234567891012", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John1", + Age: 31, + LastAccess: time.Now(), + Pass: "1234567", + Email: "jhon1@example.com", + Phone: "1234567891011", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John2", + Age: 32, + LastAccess: time.Now(), + Pass: "12345678", + Email: "jhon1@example.com", + Phone: "1234567891012", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John", + Age: 30, + LastAccess: time.Now(), + Pass: "123456", + Email: "jhon@example.com", + Phone: "123456789", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John1", + Age: 31, + LastAccess: time.Now(), + Pass: "1234567", + Email: "jhon1@example.com", + Phone: "1234567891011", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John2", + Age: 32, + LastAccess: time.Now(), + Pass: "12345678", + Email: "jhon1@example.com", + Phone: "1234567891012", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John", + Age: 30, + LastAccess: time.Now(), + Pass: "123456", + Email: "jhon@example.com", + Phone: "123456789", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John1", + Age: 31, + LastAccess: time.Now(), + Pass: "1234567", + Email: "jhon1@example.com", + Phone: "1234567891011", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John2", + Age: 32, + LastAccess: time.Now(), + Pass: "12345678", + Email: "jhon1@example.com", + Phone: "1234567891012", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John", + Age: 30, + LastAccess: time.Now(), + Pass: "123456", + Email: "jhon@example.com", + Phone: "123456789", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John1", + Age: 31, + LastAccess: time.Now(), + Pass: "1234567", + Email: "jhon1@example.com", + Phone: "1234567891011", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John2", + Age: 32, + LastAccess: time.Now(), + Pass: "12345678", + Email: "jhon1@example.com", + Phone: "1234567891012", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John1", + Age: 31, + LastAccess: time.Now(), + Pass: "1234567", + Email: "jhon1@example.com", + Phone: "1234567891011", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John2", + Age: 32, + LastAccess: time.Now(), + Pass: "12345678", + Email: "jhon1@example.com", + Phone: "1234567891012", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John", + Age: 30, + LastAccess: time.Now(), + Pass: "123456", + Email: "jhon@example.com", + Phone: "123456789", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John1", + Age: 31, + LastAccess: time.Now(), + Pass: "1234567", + Email: "jhon1@example.com", + Phone: "1234567891011", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John2", + Age: 32, + LastAccess: time.Now(), + Pass: "12345678", + Email: "jhon1@example.com", + Phone: "1234567891012", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John", + Age: 30, + LastAccess: time.Now(), + Pass: "123456", + Email: "jhon@example.com", + Phone: "123456789", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John1", + Age: 31, + LastAccess: time.Now(), + Pass: "1234567", + Email: "jhon1@example.com", + Phone: "1234567891011", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John2", + Age: 32, + LastAccess: time.Now(), + Pass: "12345678", + Email: "jhon1@example.com", + Phone: "1234567891012", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John", + Age: 30, + LastAccess: time.Now(), + Pass: "123456", + Email: "jhon@example.com", + Phone: "123456789", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John1", + Age: 31, + LastAccess: time.Now(), + Pass: "1234567", + Email: "jhon1@example.com", + Phone: "1234567891011", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John2", + Age: 32, + LastAccess: time.Now(), + Pass: "12345678", + Email: "jhon1@example.com", + Phone: "1234567891012", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + } + + hypercache.Set("key", users, 0) + + key, ok := hypercache.GetWithInfo("key") + + if !ok { + panic("key not found") + } + + fmt.Println("value", key.Value) + fmt.Println("size", key.Size) + +} + +// ` fmt.Println("size", kate.Size)` +// the first method returns 43 diff --git a/go.mod b/go.mod index c9c161a..74dbe57 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/go-redis/redis/v9 v9.0.0-rc.2 github.com/longbridgeapp/assert v1.1.0 github.com/shamaton/msgpack/v2 v2.1.1 + github.com/ugorji/go/codec v1.2.8 ) require ( diff --git a/go.sum b/go.sum index eaddf9f..e07c0e3 100644 --- a/go.sum +++ b/go.sum @@ -26,6 +26,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ 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= +github.com/ugorji/go/codec v1.2.8 h1:sgBJS6COt0b/P40VouWKdseidkDgHxYGm0SAglUHfP0= +github.com/ugorji/go/codec v1.2.8/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= diff --git a/hypercache.go b/hypercache.go index 4d19e42..005ff43 100644 --- a/hypercache.go +++ b/hypercache.go @@ -312,12 +312,19 @@ func (hyperCache *HyperCache[T]) Set(key string, value any, expiration time.Dura item.Value = value item.Expiration = expiration item.LastAccess = time.Now() + // Set the size of the item + err := item.SetSize() + + if err != nil { + models.ItemPool.Put(item) + return err + } hyperCache.mutex.Lock() defer hyperCache.mutex.Unlock() // Insert the item into the cache - err := hyperCache.backend.Set(item) + err = hyperCache.backend.Set(item) if err != nil { models.ItemPool.Put(item) return err @@ -344,6 +351,11 @@ func (hyperCache *HyperCache[T]) SetMultiple(items map[string]any, expiration ti item.Value = value item.Expiration = expiration item.LastAccess = time.Now() + // Set the size of the item + err := item.SetSize() + if err != nil { + return err + } cacheItems = append(cacheItems, item) } @@ -440,6 +452,11 @@ func (hyperCache *HyperCache[T]) GetOrSet(key string, value any, expiration time item.Value = value item.Expiration = expiration item.LastAccess = time.Now() + // Set the size of the item + err := item.SetSize() + if err != nil { + return nil, err + } // Check for invalid key, value, or duration if err := item.Valid(); err != nil { @@ -449,7 +466,7 @@ func (hyperCache *HyperCache[T]) GetOrSet(key string, value any, expiration time hyperCache.mutex.Lock() defer hyperCache.mutex.Unlock() - err := hyperCache.backend.Set(item) + err = hyperCache.backend.Set(item) if err != nil { models.ItemPool.Put(item) return nil, err diff --git a/models/item.go b/models/item.go index b86538c..623e16d 100644 --- a/models/item.go +++ b/models/item.go @@ -3,48 +3,101 @@ package models // Item represents an item in the cache. It has a key, value, expiration duration, and a last access time field. import ( + "bytes" + "encoding/gob" "strings" "sync" "sync/atomic" "time" - // "https://github.com/kelindar/binary" "github.com/hyp3rd/hypercache/errors" + // "github.com/ugorji/go/codec" +) + +var ( + // ItemPool is a pool of Item values. + ItemPool = sync.Pool{ + New: func() any { + return &Item{} + }, + } + + // buf is a buffer used to calculate the size of the item. + buf bytes.Buffer + + // encoderPool is a pool of encoders used to calculate the size of the item. + encoderPool = sync.Pool{ + New: func() any { + return gob.NewEncoder(&buf) + }, + } + + // b []byte + + // // encoderPool is a pool of encoders used to calculate the size of the item. + // encoderPool2 = sync.Pool{ + // New: func() any { + // return codec.NewEncoderBytes(&b, &codec.CborHandle{}) + // }, + // } ) // Item is a struct that represents an item in the cache. It has a key, value, expiration duration, and a last access time field. type Item struct { Key string // key of the item Value any // Value of the item + Size int // Size of the item, in bytes Expiration time.Duration // Expiration duration of the item LastAccess time.Time // LastAccess time of the item AccessCount uint // AccessCount of times the item has been accessed } -// ItemPool is a pool of Item values. -var ItemPool = sync.Pool{ - New: func() any { - return &Item{} - }, -} - -// 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) +// Size returns the size of the Item in bytes +func (i *Item) SetSize() error { + // Get an encoder from the pool + enc := encoderPool.Get().(*gob.Encoder) -// // Get the reflect.Value of the item struct itself by calling Elem() on the pointer value -// f := v.Elem().FieldByName(name) + // Encode the item + if err := enc.Encode(i.Value); err != nil { + return errors.ErrInvalidSize + } + // Set the size of the item + i.Size = buf.Len() + // Reset the buffer and put the encoder back in the pool + buf.Reset() + encoderPool.Put(enc) + return nil +} -// // If the field does not exist, return an empty reflect.Value -// if !f.IsValid() { -// return reflect.Value{} +// func (i *Item) SetSizev2() error { +// // var b []byte +// // enc := codec.NewEncoderBytes(&b, &codec.CborHandle{}) +// enc := encoderPool2.Get().(*codec.Encoder) +// if err := enc.Encode(i.Value); err != nil { +// return errors.ErrInvalidSize // } -// // Return the field value -// return f +// i.Size = len(b) +// b = b[:0] +// encoderPool2.Put(enc) +// return nil // } +// SizeMB returns the size of the Item in megabytes +func (i *Item) SizeMB() float64 { + return float64(i.Size) / (1024 * 1024) +} + +// SizeKB returns the size of the Item in kilobytes +func (i *Item) SizeKB() float64 { + return float64(i.Size) / 1024 +} + +// Touch updates the last access time of the item and increments the access count. +func (item *Item) Touch() { + item.LastAccess = time.Now() + item.AccessCount++ +} + // Valid returns an error if the item is invalid, nil otherwise. func (item *Item) Valid() error { // Check for empty key @@ -65,44 +118,8 @@ func (item *Item) Valid() error { return nil } -// Touch updates the last access time of the item and increments the access count. -func (item *Item) Touch() { - item.LastAccess = time.Now() - item.AccessCount++ -} - // Expired returns true if the item has expired, false otherwise. func (item *Item) Expired() bool { // If the expiration duration is 0, the item never expires return item.Expiration > 0 && time.Since(item.LastAccess) > item.Expiration } - -// MarshalBinary implements the encoding.BinaryMarshaler interface. -// func (item *Item) MarshalBinary() (data []byte, err error) { -// buf := bytes.NewBuffer([]byte{}) -// enc := gob.NewEncoder(buf) -// err = enc.Encode(item) -// if err != nil { -// return nil, err -// } -// return buf.Bytes(), nil -// } - -// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. -// -// func (item *Item) UnmarshalBinary(data []byte) error { -// buf := bytes.NewBuffer(data) -// dec := gob.NewDecoder(buf) -// return dec.Decode(item) -// } -// - -// MarshalBinary implements the encoding.BinaryMarshaler interface. -// 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) -// } From 11e9cd7af3e928f5eadc1e4e190c4cad227cc1d8 Mon Sep 17 00:00:00 2001 From: Francesco Cosentino Date: Sat, 21 Jan 2023 22:07:01 +0100 Subject: [PATCH 2/9] first implementation tested with the in-memory backend --- backend/backend.go | 4 ++- backend/inmemory.go | 67 ++++++++++++++++++------------------ backend/redis.go | 50 +++++++++++++++++++-------- examples/size/size.go | 80 ++++++++++++++++++++++++++++++++++++++----- go.mod | 1 - go.sum | 2 -- hypercache.go | 44 ++++++------------------ 7 files changed, 154 insertions(+), 94 deletions(-) diff --git a/backend/backend.go b/backend/backend.go index d56445f..b434f74 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -41,8 +41,10 @@ type IBackend[T IBackendConstrain] interface { Capacity() int // SetCapacity sets the maximum number of items that can be stored in the cache. SetCapacity(capacity int) - // Size returns the number of items currently stored in the cache. + // Size returns the size in bytes of items currently stored in the cache. Size() int + // Count returns the number of items currently stored in the cache. + Count() int // Remove deletes the item with the given key from the cache. Remove(keys ...string) error } diff --git a/backend/inmemory.go b/backend/inmemory.go index 895e8c7..c2e8832 100644 --- a/backend/inmemory.go +++ b/backend/inmemory.go @@ -13,26 +13,26 @@ import ( // InMemory is a cache backend that stores the items in memory, leveraging a custom `ConcurrentMap`. type InMemory struct { - items datastructure.ConcurrentMap[string, *models.Item] // map to store the items in the cache - capacity int // capacity of the cache, limits the number of items that can be stored in the cache - maxCacheSize int // maxCacheSize instructs the cache not allocate more memory than this limit, value in MB, 0 means no limit - mutex sync.RWMutex // mutex to protect the cache from concurrent access - SortFilters // filters applied when listing the items in the cache + items datastructure.ConcurrentMap[string, *models.Item] // map to store the items in the cache + capacity int // capacity of the cache, limits the number of items that can be stored in the cache + maxCacheSize int // maxCacheSize instructs the cache not allocate more memory than this limit, value in MB, 0 means no limit + memoryAllocation int // memoryAllocation is the current memory allocation of the cache, value in bytes + mutex sync.RWMutex // mutex to protect the cache from concurrent access + SortFilters // filters applied when listing the items in the cache } // NewInMemory creates a new in-memory cache with the given options. func NewInMemory[T InMemory](opts ...Option[InMemory]) (backend IInMemory[T], err error) { - InMemory := &InMemory{ items: datastructure.New[*models.Item](), } - + // Apply the backend options ApplyOptions(InMemory, opts...) - + // Check if the `capacity` is valid if InMemory.capacity < 0 { return nil, errors.ErrInvalidCapacity } - + // Check if the `maxCacheSize` is valid if InMemory.maxCacheSize < 0 { return nil, errors.ErrInvalidMaxCacheSize } @@ -45,22 +45,35 @@ func (cacheBackend *InMemory) SetCapacity(capacity int) { if capacity < 0 { return } - cacheBackend.capacity = capacity } -// itemCount returns the number of items in the cache. -func (cacheBackend *InMemory) itemCount() int { +// Capacity returns the capacity of the cacheBackend. +func (cacheBackend *InMemory) Capacity() int { + return cacheBackend.capacity +} + +// Count returns the number of items in the cache. +func (cacheBackend *InMemory) Count() int { return cacheBackend.items.Count() } +// Size returns the number of items in the cacheBackend. +func (cacheBackend *InMemory) Size() int { + return cacheBackend.memoryAllocation +} + +// MaxCacheSize returns the maximum size in bytes of the cacheBackend. +func (cacheBackend *InMemory) MaxCacheSize() int { + return cacheBackend.maxCacheSize +} + // Get retrieves the item with the given key from the cacheBackend. If the item is not found, it returns nil. func (cacheBackend *InMemory) Get(key string) (item *models.Item, ok bool) { item, ok = cacheBackend.items.Get(key) if !ok { return nil, false } - // return the item return item, true } @@ -75,11 +88,15 @@ func (cacheBackend *InMemory) Set(item *models.Item) error { cacheBackend.mutex.Lock() defer cacheBackend.mutex.Unlock() + // Set the size of the item + err := item.SetSize() + if err != nil { + return err + } // check if adding this item will exceed the maxCacheSize - currentSize := cacheBackend.maxCacheSize - newSize := currentSize + item.Size - if cacheBackend.maxCacheSize > 0 && newSize > cacheBackend.maxCacheSize { + cacheBackend.memoryAllocation = cacheBackend.memoryAllocation + item.Size + if cacheBackend.maxCacheSize > 0 && cacheBackend.memoryAllocation > cacheBackend.maxCacheSize { return errors.ErrCacheFull } @@ -140,21 +157,3 @@ func (cacheBackend *InMemory) Clear() { cacheBackend.items.Remove(item.Key) } } - -// Capacity returns the capacity of the cacheBackend. -func (cacheBackend *InMemory) Capacity() int { - return cacheBackend.capacity -} - -// Size returns the number of items in the cacheBackend. -func (cacheBackend *InMemory) Size() int { - return cacheBackend.itemCount() -} - -func (cacheBackend *InMemory) SetMaxCacheSize(maxCacheSize int) { - cacheBackend.maxCacheSize = maxCacheSize -} - -func (cacheBackend *InMemory) MaxCacheSize() int { - return cacheBackend.maxCacheSize -} diff --git a/backend/redis.go b/backend/redis.go index 6c7776f..fd6084e 100644 --- a/backend/redis.go +++ b/backend/redis.go @@ -14,10 +14,11 @@ import ( // 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 - maxCacheSize int // maxCacheSize is the maximum number of items that can be stored 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 + keysSetName string // keysSetName is the name of the set that holds the keys of the items in the cache + maxCacheSize int // maxCacheSize is the maximum number of items that can be stored in the cache + memoryAllocation int // memoryAllocation is the current memory allocation of the cache, value in bytes // 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 @@ -37,7 +38,10 @@ func NewRedisBackend[T Redis](redisOptions ...Option[Redis]) (backend IRedisBack if rb.capacity < 0 { return nil, errors.ErrInvalidCapacity } - + // Check if the `maxCacheSize` is valid + if rb.maxCacheSize < 0 { + return nil, errors.ErrInvalidMaxCacheSize + } // Check if the `keysSetName` is empty if rb.keysSetName == "" { rb.keysSetName = "hypercache" @@ -56,11 +60,6 @@ func NewRedisBackend[T Redis](redisOptions ...Option[Redis]) (backend IRedisBack return rb, nil } -// Capacity returns the maximum number of items that can be stored in the cache. -func (cacheBackend *Redis) Capacity() int { - return cacheBackend.capacity -} - // SetCapacity sets the capacity of the cache. func (cacheBackend *Redis) SetCapacity(capacity int) { if capacity < 0 { @@ -69,15 +68,25 @@ func (cacheBackend *Redis) SetCapacity(capacity int) { cacheBackend.capacity = capacity } -// itemCount returns the number of items in the cache. -func (cacheBackend *Redis) itemCount() int { +// Capacity returns the maximum number of items that can be stored in the cache. +func (cacheBackend *Redis) Capacity() int { + return cacheBackend.capacity +} + +// Size returns the number of items in the cache. +func (cacheBackend *Redis) Count() int { count, _ := cacheBackend.rdb.DBSize(context.Background()).Result() return int(count) } -// Size returns the number of items in the cache. +// Size returns the number of items in the cacheBackend. func (cacheBackend *Redis) Size() int { - return cacheBackend.itemCount() + return cacheBackend.memoryAllocation +} + +// MaxCacheSize returns the maximum size in bytes of the cacheBackend. +func (cacheBackend *Redis) MaxCacheSize() int { + return cacheBackend.maxCacheSize } // Get retrieves the Item with the given key from the cacheBackend. If the item is not found, it returns nil. @@ -97,6 +106,7 @@ func (cacheBackend *Redis) Get(key string) (item *models.Item, ok bool) { item = models.ItemPool.Get().(*models.Item) // 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()) @@ -125,6 +135,18 @@ func (cacheBackend *Redis) Set(item *models.Item) error { return err } + // Set the size of the item + err := item.SetSize() + if err != nil { + return err + } + + // check if adding this item will exceed the maxCacheSize + cacheBackend.memoryAllocation = cacheBackend.memoryAllocation + item.Size + if cacheBackend.maxCacheSize > 0 && cacheBackend.memoryAllocation > cacheBackend.maxCacheSize { + return errors.ErrCacheFull + } + // Serialize the item data, err := cacheBackend.Serializer.Marshal(item) if err != nil { diff --git a/examples/size/size.go b/examples/size/size.go index b4fe019..a789b3f 100644 --- a/examples/size/size.go +++ b/examples/size/size.go @@ -5,10 +5,24 @@ import ( "time" "github.com/hyp3rd/hypercache" + "github.com/hyp3rd/hypercache/backend" ) func main() { - hypercache, err := hypercache.NewInMemoryWithDefaults(10) + config := hypercache.NewConfig[backend.InMemory]() + + config.HyperCacheOptions = []hypercache.Option[backend.InMemory]{ + hypercache.WithEvictionInterval[backend.InMemory](0), + hypercache.WithEvictionAlgorithm[backend.InMemory]("cawolfu"), + } + + config.InMemoryOptions = []backend.Option[backend.InMemory]{ + backend.WithCapacity[backend.InMemory](100000), + backend.WithMaxCacheSize[backend.InMemory](7326), + } + + // Create a new HyperCache with a capacity of 10 + cache, err := hypercache.New(config) if err != nil { panic(err) @@ -522,19 +536,67 @@ func main() { IsGuest: bool(false), IsBanned: bool(false), }, + { + Name: "John2", + Age: 32, + LastAccess: time.Now(), + Pass: "12345678", + Email: "jhon1@example.com", + Phone: "1234567891012", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John2John2John2John2John2John2John2John2John2John2John2", + Age: 32, + LastAccess: time.Now(), + Pass: "12345678", + Email: "jhon1@example.com", + Phone: "1234567891012", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John2", + Age: 32, + LastAccess: time.Now(), + Pass: "12345678", + Email: "jhon1@example.com", + Phone: "1234567891012", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, + { + Name: "John2", + Age: 32, + LastAccess: time.Now(), + Pass: "12345678", + Email: "jhon1@example.com", + Phone: "1234567891012", + IsAdmin: bool(true), + IsGuest: bool(false), + IsBanned: bool(false), + }, } - hypercache.Set("key", users, 0) - - key, ok := hypercache.GetWithInfo("key") - - if !ok { - panic("key not found") + for i := 0; i < 3; i++ { + err = cache.Set(fmt.Sprintf("key-%d", i), users, 0) + if err != nil { + fmt.Println(err, "set", i) + } } - fmt.Println("value", key.Value) - fmt.Println("size", key.Size) + key, ok := cache.GetWithInfo("key-1") + if ok { + fmt.Println("value", key.Value) + fmt.Println("size", key.Size) + } else { + fmt.Println("key not found") + } } // ` fmt.Println("size", kate.Size)` diff --git a/go.mod b/go.mod index 74dbe57..c9c161a 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ require ( github.com/go-redis/redis/v9 v9.0.0-rc.2 github.com/longbridgeapp/assert v1.1.0 github.com/shamaton/msgpack/v2 v2.1.1 - github.com/ugorji/go/codec v1.2.8 ) require ( diff --git a/go.sum b/go.sum index e07c0e3..eaddf9f 100644 --- a/go.sum +++ b/go.sum @@ -26,8 +26,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ 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= -github.com/ugorji/go/codec v1.2.8 h1:sgBJS6COt0b/P40VouWKdseidkDgHxYGm0SAglUHfP0= -github.com/ugorji/go/codec v1.2.8/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= diff --git a/hypercache.go b/hypercache.go index 005ff43..72e69f2 100644 --- a/hypercache.go +++ b/hypercache.go @@ -57,7 +57,8 @@ type HyperCache[T backend.IBackendConstrain] struct { // - The eviction interval is set to 10 minutes. // - The eviction algorithm is set to LRU. // - The expiration interval is set to 30 minutes. -// - The capacity of the in-memory backend is set to 1000 items. +// - The capacity of the in-memory backend is set to 0 items (no limitations) unless specified. +// - The maximum cache size in bytes is set to 0 (no limitations). func NewInMemoryWithDefaults(capacity int) (hyperCache *HyperCache[backend.InMemory], err error) { // Initialize the configuration config := NewConfig[backend.InMemory]() @@ -239,7 +240,7 @@ func (hyperCache *HyperCache[T]) expirationLoop() { hyperCache.StatsCollector.Incr("item_expired_count", 1) } - hyperCache.StatsCollector.Gauge("item_count", int64(hyperCache.backend.Size())) + hyperCache.StatsCollector.Gauge("item_count", int64(hyperCache.backend.Count())) hyperCache.StatsCollector.Gauge("expired_item_count", expiredCount) } @@ -251,7 +252,7 @@ func (hyperCache *HyperCache[T]) evictionLoop() { var evictedCount int64 for { - if hyperCache.backend.Size() <= hyperCache.backend.Capacity() { + if hyperCache.backend.Count() <= hyperCache.backend.Capacity() { break } @@ -271,7 +272,7 @@ func (hyperCache *HyperCache[T]) evictionLoop() { hyperCache.StatsCollector.Incr("item_evicted_count", 1) } - hyperCache.StatsCollector.Gauge("item_count", int64(hyperCache.backend.Size())) + hyperCache.StatsCollector.Gauge("item_count", int64(hyperCache.backend.Count())) hyperCache.StatsCollector.Gauge("evicted_item_count", evictedCount) } @@ -297,7 +298,7 @@ func (hyperCache *HyperCache[T]) SetCapacity(capacity int) { // set capacity of the backend hyperCache.backend.SetCapacity(capacity) // if the cache size is greater than the new capacity, evict items - if hyperCache.backend.Size() > hyperCache.Capacity() { + if hyperCache.backend.Count() > hyperCache.Capacity() { hyperCache.evictionLoop() } } @@ -312,19 +313,12 @@ func (hyperCache *HyperCache[T]) Set(key string, value any, expiration time.Dura item.Value = value item.Expiration = expiration item.LastAccess = time.Now() - // Set the size of the item - err := item.SetSize() - - if err != nil { - models.ItemPool.Put(item) - return err - } hyperCache.mutex.Lock() defer hyperCache.mutex.Unlock() // Insert the item into the cache - err = hyperCache.backend.Set(item) + err := hyperCache.backend.Set(item) if err != nil { models.ItemPool.Put(item) return err @@ -334,7 +328,7 @@ func (hyperCache *HyperCache[T]) Set(key string, value any, expiration time.Dura hyperCache.evictionAlgorithm.Set(key, item.Value) // If the cache is at capacity, evict an item when the eviction interval is zero - if hyperCache.evictionInterval == 0 && hyperCache.backend.Capacity() > 0 && hyperCache.backend.Size() > hyperCache.backend.Capacity() { + if hyperCache.evictionInterval == 0 && hyperCache.backend.Capacity() > 0 && hyperCache.backend.Count() > hyperCache.backend.Capacity() { hyperCache.evictItem() } @@ -351,11 +345,6 @@ func (hyperCache *HyperCache[T]) SetMultiple(items map[string]any, expiration ti item.Value = value item.Expiration = expiration item.LastAccess = time.Now() - // Set the size of the item - err := item.SetSize() - if err != nil { - return err - } cacheItems = append(cacheItems, item) } @@ -376,7 +365,7 @@ func (hyperCache *HyperCache[T]) SetMultiple(items map[string]any, expiration ti } // If the cache is at capacity, evict an item when the eviction interval is zero - if hyperCache.evictionInterval == 0 && hyperCache.backend.Capacity() > 0 && hyperCache.backend.Size() > hyperCache.backend.Capacity() { + if hyperCache.evictionInterval == 0 && hyperCache.backend.Capacity() > 0 && hyperCache.backend.Count() > hyperCache.backend.Capacity() { hyperCache.evictionLoop() } @@ -452,21 +441,10 @@ func (hyperCache *HyperCache[T]) GetOrSet(key string, value any, expiration time item.Value = value item.Expiration = expiration item.LastAccess = time.Now() - // Set the size of the item - err := item.SetSize() - if err != nil { - return nil, err - } - - // Check for invalid key, value, or duration - if err := item.Valid(); err != nil { - models.ItemPool.Put(item) - return nil, err - } hyperCache.mutex.Lock() defer hyperCache.mutex.Unlock() - err = hyperCache.backend.Set(item) + err := hyperCache.backend.Set(item) if err != nil { models.ItemPool.Put(item) return nil, err @@ -609,7 +587,7 @@ func (hyperCache *HyperCache[T]) Capacity() int { // Size returns the number of items in the cache. func (hyperCache *HyperCache[T]) Size() int { - return hyperCache.backend.Size() + return hyperCache.backend.Count() } // TriggerEviction sends a signal to the eviction loop to start. From 3be50c939fbc7f6bed597b1432a2e8c8d4d3c3ab Mon Sep 17 00:00:00 2001 From: Francesco Cosentino Date: Sat, 21 Jan 2023 22:11:25 +0100 Subject: [PATCH 3/9] removed SortByValue from the types --- examples/eviction/eviction.go | 4 ++-- examples/stats/stats.go | 2 +- types/enums.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/eviction/eviction.go b/examples/eviction/eviction.go index c68c336..220dc61 100644 --- a/examples/eviction/eviction.go +++ b/examples/eviction/eviction.go @@ -60,7 +60,7 @@ func executeExample(evictionInterval time.Duration) { log.Println("capacity after adding 15 items", cache.Capacity()) log.Println("listing all items in the cache") - list, err := cache.List(backend.WithSortBy[backend.InMemory](types.SortByValue)) + list, err := cache.List(backend.WithSortBy[backend.InMemory](types.SortByKey)) if err != nil { fmt.Println(err) return @@ -75,7 +75,7 @@ func executeExample(evictionInterval time.Duration) { fmt.Println("sleeping to allow the evition loop to complete", evictionInterval+2*time.Second) time.Sleep(evictionInterval + 2*time.Second) log.Println("listing all items in the cache the eviction is triggered") - list, err = cache.List(backend.WithSortBy[backend.InMemory](types.SortByValue)) + list, err = cache.List(backend.WithSortBy[backend.InMemory](types.SortByKey)) if err != nil { fmt.Println(err) return diff --git a/examples/stats/stats.go b/examples/stats/stats.go index 51a647d..c15ef1b 100644 --- a/examples/stats/stats.go +++ b/examples/stats/stats.go @@ -51,7 +51,7 @@ func main() { // Retrieve the list of items from the cache list, err := hyperCache.List( - backend.WithSortBy[backend.InMemory](types.SortByValue), + backend.WithSortBy[backend.InMemory](types.SortByKey), backend.WithSortOrderAsc[backend.InMemory](true), backend.WithFilterFunc[backend.InMemory](func(item *models.Item) bool { return item.Expiration > time.Second diff --git a/types/enums.go b/types/enums.go index c4308bc..2259eba 100644 --- a/types/enums.go +++ b/types/enums.go @@ -6,7 +6,7 @@ type SortingField string // Constants for the different fields that the cache items can be sorted by. const ( SortByKey SortingField = "Key" // Sort by the key of the cache item - SortByValue SortingField = "Value" // Sort by the value of the cache item + SortBySize SortingField = "Size" // Sort by the size in bytes of the cache item SortByLastAccess SortingField = "LastAccess" // Sort by the last access time of the cache item SortByAccessCount SortingField = "AccessCount" // Sort by the number of times the cache item has been accessed SortByExpiration SortingField = "Expiration" // Sort by the expiration duration of the cache item From f67e7c5d18e410f2500ddde39c0dc1635b0e9806 Mon Sep 17 00:00:00 2001 From: Francesco Cosentino Date: Tue, 24 Jan 2023 15:39:19 +0100 Subject: [PATCH 4/9] improved map --- backend/inmemory.go | 50 ++++-- backend/inmemory.go.bak | 168 ++++++++++++++++++ backend/inmemory.go.bak3 | 174 ++++++++++++++++++ datastructure/cmap.go | 24 +-- datastructure/v2/cmap.go | 373 +++++++++++++++++++++++++++++++++++++++ datastructure/v3/cmap.go | 198 +++++++++++++++++++++ examples/list/list.go | 2 +- 7 files changed, 960 insertions(+), 29 deletions(-) create mode 100644 backend/inmemory.go.bak create mode 100644 backend/inmemory.go.bak3 create mode 100644 datastructure/v2/cmap.go create mode 100644 datastructure/v3/cmap.go diff --git a/backend/inmemory.go b/backend/inmemory.go index c2e8832..2d8b658 100644 --- a/backend/inmemory.go +++ b/backend/inmemory.go @@ -5,7 +5,7 @@ import ( "sort" "sync" - "github.com/hyp3rd/hypercache/datastructure" + datastructure "github.com/hyp3rd/hypercache/datastructure/v3" "github.com/hyp3rd/hypercache/errors" "github.com/hyp3rd/hypercache/models" "github.com/hyp3rd/hypercache/types" @@ -13,18 +13,20 @@ import ( // InMemory is a cache backend that stores the items in memory, leveraging a custom `ConcurrentMap`. type InMemory struct { - items datastructure.ConcurrentMap[string, *models.Item] // map to store the items in the cache - capacity int // capacity of the cache, limits the number of items that can be stored in the cache - maxCacheSize int // maxCacheSize instructs the cache not allocate more memory than this limit, value in MB, 0 means no limit - memoryAllocation int // memoryAllocation is the current memory allocation of the cache, value in bytes - mutex sync.RWMutex // mutex to protect the cache from concurrent access - SortFilters // filters applied when listing the items in the cache + // items datastructure.ConcurrentMap[string, *models.Item] // map to store the items in the cache + items datastructure.ConcurrentMap // map to store the items in the cache + capacity int // capacity of the cache, limits the number of items that can be stored in the cache + maxCacheSize int // maxCacheSize instructs the cache not allocate more memory than this limit, value in MB, 0 means no limit + memoryAllocation int // memoryAllocation is the current memory allocation of the cache, value in bytes + mutex sync.RWMutex // mutex to protect the cache from concurrent access + SortFilters // filters applied when listing the items in the cache } // NewInMemory creates a new in-memory cache with the given options. func NewInMemory[T InMemory](opts ...Option[InMemory]) (backend IInMemory[T], err error) { InMemory := &InMemory{ - items: datastructure.New[*models.Item](), + // items: datastructure.New[*models.Item](), + items: datastructure.New(), } // Apply the backend options ApplyOptions(InMemory, opts...) @@ -109,12 +111,19 @@ func (cacheBackend *InMemory) List(options ...FilterOption[InMemory]) ([]*models // Apply the filter options ApplyFilterOptions(cacheBackend, options...) - items := make([]*models.Item, 0) + items := make([]*models.Item, 0, cacheBackend.items.Count()) + wg := sync.WaitGroup{} + wg.Add(cacheBackend.items.Count()) for item := range cacheBackend.items.IterBuffered() { - if cacheBackend.FilterFunc == nil || cacheBackend.FilterFunc(item.Val) { - items = append(items, item.Val) - } + // go func(item datastructure.Tuple[string, *models.Item]) { + go func(item datastructure.Tuple) { + defer wg.Done() + if cacheBackend.FilterFunc == nil || cacheBackend.FilterFunc(&item.Val) { + items = append(items, &item.Val) + } + }(item) } + wg.Wait() if cacheBackend.SortBy == "" { return items, nil @@ -145,15 +154,24 @@ func (cacheBackend *InMemory) List(options ...FilterOption[InMemory]) ([]*models // Remove removes items with the given key from the cacheBackend. If an item is not found, it does nothing. func (cacheBackend *InMemory) Remove(keys ...string) (err error) { //TODO: determine if handling the error or not + // var ok bool + // item := models.ItemPool.Get().(*models.Item) + // defer models.ItemPool.Put(item) for _, key := range keys { - cacheBackend.items.Remove(key) + item, ok := cacheBackend.items.Get(key) + if ok { + // remove the item from the cacheBackend and update the memory allocation + cacheBackend.memoryAllocation = cacheBackend.memoryAllocation - item.Size + cacheBackend.items.Remove(key) + } } return } // Clear removes all items from the cacheBackend. func (cacheBackend *InMemory) Clear() { - for item := range cacheBackend.items.IterBuffered() { - cacheBackend.items.Remove(item.Key) - } + // clear the cacheBackend + cacheBackend.items.Clear() + // reset the memory allocation + cacheBackend.memoryAllocation = 0 } diff --git a/backend/inmemory.go.bak b/backend/inmemory.go.bak new file mode 100644 index 0000000..52c42c6 --- /dev/null +++ b/backend/inmemory.go.bak @@ -0,0 +1,168 @@ +package backend + +import ( + "fmt" + "sort" + "sync" + + "github.com/hyp3rd/hypercache/datastructure" + "github.com/hyp3rd/hypercache/errors" + "github.com/hyp3rd/hypercache/models" + "github.com/hyp3rd/hypercache/types" +) + +// InMemory is a cache backend that stores the items in memory, leveraging a custom `ConcurrentMap`. +type InMemory struct { + items datastructure.ConcurrentMap[string, *models.Item] // map to store the items in the cache + capacity int // capacity of the cache, limits the number of items that can be stored in the cache + maxCacheSize int // maxCacheSize instructs the cache not allocate more memory than this limit, value in MB, 0 means no limit + memoryAllocation int // memoryAllocation is the current memory allocation of the cache, value in bytes + mutex sync.RWMutex // mutex to protect the cache from concurrent access + SortFilters // filters applied when listing the items in the cache +} + +// NewInMemory creates a new in-memory cache with the given options. +func NewInMemory[T InMemory](opts ...Option[InMemory]) (backend IInMemory[T], err error) { + InMemory := &InMemory{ + items: datastructure.New[*models.Item](), + } + // Apply the backend options + ApplyOptions(InMemory, opts...) + // Check if the `capacity` is valid + if InMemory.capacity < 0 { + return nil, errors.ErrInvalidCapacity + } + // Check if the `maxCacheSize` is valid + if InMemory.maxCacheSize < 0 { + return nil, errors.ErrInvalidMaxCacheSize + } + + return InMemory, nil +} + +// SetCapacity sets the capacity of the cache. +func (cacheBackend *InMemory) SetCapacity(capacity int) { + if capacity < 0 { + return + } + cacheBackend.capacity = capacity +} + +// Capacity returns the capacity of the cacheBackend. +func (cacheBackend *InMemory) Capacity() int { + return cacheBackend.capacity +} + +// Count returns the number of items in the cache. +func (cacheBackend *InMemory) Count() int { + return cacheBackend.items.Count() +} + +// Size returns the number of items in the cacheBackend. +func (cacheBackend *InMemory) Size() int { + return cacheBackend.memoryAllocation +} + +// MaxCacheSize returns the maximum size in bytes of the cacheBackend. +func (cacheBackend *InMemory) MaxCacheSize() int { + return cacheBackend.maxCacheSize +} + +// Get retrieves the item with the given key from the cacheBackend. If the item is not found, it returns nil. +func (cacheBackend *InMemory) Get(key string) (item *models.Item, ok bool) { + item, ok = cacheBackend.items.Get(key) + if !ok { + return nil, false + } + // return the item + return item, true +} + +// Set adds a Item to the cache. +func (cacheBackend *InMemory) Set(item *models.Item) error { + // Check for invalid key, value, or duration + if err := item.Valid(); err != nil { + models.ItemPool.Put(item) + return err + } + + cacheBackend.mutex.Lock() + defer cacheBackend.mutex.Unlock() + // Set the size of the item + err := item.SetSize() + if err != nil { + return err + } + + // check if adding this item will exceed the maxCacheSize + cacheBackend.memoryAllocation = cacheBackend.memoryAllocation + item.Size + if cacheBackend.maxCacheSize > 0 && cacheBackend.memoryAllocation > cacheBackend.maxCacheSize { + return errors.ErrCacheFull + } + + cacheBackend.items.Set(item.Key, item) + return nil +} + +// List returns a list of all items in the cache filtered and ordered by the given options +func (cacheBackend *InMemory) List(options ...FilterOption[InMemory]) ([]*models.Item, error) { + // Apply the filter options + ApplyFilterOptions(cacheBackend, options...) + + items := make([]*models.Item, 0, cacheBackend.items.Count()) + for item := range cacheBackend.items.IterBuffered() { + if cacheBackend.FilterFunc == nil || cacheBackend.FilterFunc(item.Val) { + items = append(items, item.Val) + } + } + + if cacheBackend.SortBy == "" { + return items, nil + } + + var sorter sort.Interface + switch cacheBackend.SortBy { + case types.SortByKey.String(): + sorter = &itemSorterByKey{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 +} + +// Remove removes items with the given key from the cacheBackend. If an item is not found, it does nothing. +func (cacheBackend *InMemory) Remove(keys ...string) (err error) { + //TODO: determine if handling the error or not + var ok bool + item := models.ItemPool.Get().(*models.Item) + defer models.ItemPool.Put(item) + for _, key := range keys { + item, ok = cacheBackend.items.Get(key) + if ok { + // remove the item from the cacheBackend and update the memory allocation + cacheBackend.memoryAllocation = cacheBackend.memoryAllocation - item.Size + cacheBackend.items.Remove(key) + } + } + return +} + +// Clear removes all items from the cacheBackend. +func (cacheBackend *InMemory) Clear() { + // clear the cacheBackend + cacheBackend.items.Clear() + // reset the memory allocation + cacheBackend.memoryAllocation = 0 +} diff --git a/backend/inmemory.go.bak3 b/backend/inmemory.go.bak3 new file mode 100644 index 0000000..98113ba --- /dev/null +++ b/backend/inmemory.go.bak3 @@ -0,0 +1,174 @@ +package backend + +import ( + "fmt" + "sort" + "sync" + + datastructure "github.com/hyp3rd/hypercache/datastructure/v2" + "github.com/hyp3rd/hypercache/errors" + "github.com/hyp3rd/hypercache/models" + "github.com/hyp3rd/hypercache/types" +) + +// InMemory is a cache backend that stores the items in memory, leveraging a custom `ConcurrentMap`. +type InMemory struct { + items datastructure.ConcurrentMap[string, *models.Item] // map to store the items in the cache + capacity int // capacity of the cache, limits the number of items that can be stored in the cache + maxCacheSize int // maxCacheSize instructs the cache not allocate more memory than this limit, value in MB, 0 means no limit + memoryAllocation int // memoryAllocation is the current memory allocation of the cache, value in bytes + mutex sync.RWMutex // mutex to protect the cache from concurrent access + SortFilters // filters applied when listing the items in the cache +} + +// NewInMemory creates a new in-memory cache with the given options. +func NewInMemory[T InMemory](opts ...Option[InMemory]) (backend IInMemory[T], err error) { + InMemory := &InMemory{ + items: datastructure.New[*models.Item](), + } + // Apply the backend options + ApplyOptions(InMemory, opts...) + // Check if the `capacity` is valid + if InMemory.capacity < 0 { + return nil, errors.ErrInvalidCapacity + } + // Check if the `maxCacheSize` is valid + if InMemory.maxCacheSize < 0 { + return nil, errors.ErrInvalidMaxCacheSize + } + + return InMemory, nil +} + +// SetCapacity sets the capacity of the cache. +func (cacheBackend *InMemory) SetCapacity(capacity int) { + if capacity < 0 { + return + } + cacheBackend.capacity = capacity +} + +// Capacity returns the capacity of the cacheBackend. +func (cacheBackend *InMemory) Capacity() int { + return cacheBackend.capacity +} + +// Count returns the number of items in the cache. +func (cacheBackend *InMemory) Count() int { + return cacheBackend.items.Count() +} + +// Size returns the number of items in the cacheBackend. +func (cacheBackend *InMemory) Size() int { + return cacheBackend.memoryAllocation +} + +// MaxCacheSize returns the maximum size in bytes of the cacheBackend. +func (cacheBackend *InMemory) MaxCacheSize() int { + return cacheBackend.maxCacheSize +} + +// Get retrieves the item with the given key from the cacheBackend. If the item is not found, it returns nil. +func (cacheBackend *InMemory) Get(key string) (item *models.Item, ok bool) { + item, ok = cacheBackend.items.Get(key) + if !ok { + return nil, false + } + // return the item + return item, true +} + +// Set adds a Item to the cache. +func (cacheBackend *InMemory) Set(item *models.Item) error { + // Check for invalid key, value, or duration + if err := item.Valid(); err != nil { + models.ItemPool.Put(item) + return err + } + + cacheBackend.mutex.Lock() + defer cacheBackend.mutex.Unlock() + // Set the size of the item + err := item.SetSize() + if err != nil { + return err + } + + // check if adding this item will exceed the maxCacheSize + cacheBackend.memoryAllocation = cacheBackend.memoryAllocation + item.Size + if cacheBackend.maxCacheSize > 0 && cacheBackend.memoryAllocation > cacheBackend.maxCacheSize { + return errors.ErrCacheFull + } + + cacheBackend.items.Set(item.Key, item) + return nil +} + +// List returns a list of all items in the cache filtered and ordered by the given options +func (cacheBackend *InMemory) List(options ...FilterOption[InMemory]) ([]*models.Item, error) { + // Apply the filter options + ApplyFilterOptions(cacheBackend, options...) + + items := make([]*models.Item, 0, cacheBackend.items.Count()) + wg := sync.WaitGroup{} + wg.Add(cacheBackend.items.Count()) + for item := range cacheBackend.items.IterBuffered() { + go func(item datastructure.Tuple[string, *models.Item]) { + defer wg.Done() + if cacheBackend.FilterFunc == nil || cacheBackend.FilterFunc(item.Val) { + items = append(items, item.Val) + } + }(item) + } + wg.Wait() + + if cacheBackend.SortBy == "" { + return items, nil + } + + var sorter sort.Interface + switch cacheBackend.SortBy { + case types.SortByKey.String(): + sorter = &itemSorterByKey{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 +} + +// Remove removes items with the given key from the cacheBackend. If an item is not found, it does nothing. +func (cacheBackend *InMemory) Remove(keys ...string) (err error) { + //TODO: determine if handling the error or not + // var ok bool + // item := models.ItemPool.Get().(*models.Item) + // defer models.ItemPool.Put(item) + for _, key := range keys { + item, ok := cacheBackend.items.Get(key) + if ok { + // remove the item from the cacheBackend and update the memory allocation + cacheBackend.memoryAllocation = cacheBackend.memoryAllocation - item.Size + cacheBackend.items.Remove(key) + } + } + return +} + +// Clear removes all items from the cacheBackend. +func (cacheBackend *InMemory) Clear() { + // clear the cacheBackend + cacheBackend.items.Clear() + // reset the memory allocation + cacheBackend.memoryAllocation = 0 +} diff --git a/datastructure/cmap.go b/datastructure/cmap.go index 7cc5344..4ffa73e 100644 --- a/datastructure/cmap.go +++ b/datastructure/cmap.go @@ -18,18 +18,18 @@ type Stringer interface { // ConcurrentMap is a "thread" safe map of type string:Anything. // To avoid lock bottlenecks this map is dived to several (ShardCount) map shards. -type ConcurrentMap[K comparable, V any] struct { +type ConcurrentMap[K comparable, V interface{}] struct { shards []*ConcurrentMapShared[K, V] sharding func(key K) uint32 } // ConcurrentMapShared is a "thread" safe string to anything map. -type ConcurrentMapShared[K comparable, V any] struct { +type ConcurrentMapShared[K comparable, V interface{}] struct { items map[K]V sync.RWMutex // Read Write mutex, guards access to internal map. } -func create[K comparable, V any](sharding func(key K) uint32) ConcurrentMap[K, V] { +func create[K comparable, V interface{}](sharding func(key K) uint32) ConcurrentMap[K, V] { m := ConcurrentMap[K, V]{ sharding: sharding, shards: make([]*ConcurrentMapShared[K, V], ShardCount), @@ -41,17 +41,17 @@ func create[K comparable, V any](sharding func(key K) uint32) ConcurrentMap[K, V } // New creates a new concurrent map. -func New[V any]() ConcurrentMap[string, V] { +func New[V interface{}]() ConcurrentMap[string, V] { return create[string, V](fnv32) } // NewStringer creates a new concurrent map. -func NewStringer[K Stringer, V any]() ConcurrentMap[K, V] { +func NewStringer[K Stringer, V interface{}]() ConcurrentMap[K, V] { return create[K, V](strfnv32[K]) } // NewWithCustomShardingFunction creates a new concurrent map. -func NewWithCustomShardingFunction[K comparable, V any](sharding func(key K) uint32) ConcurrentMap[K, V] { +func NewWithCustomShardingFunction[K comparable, V interface{}](sharding func(key K) uint32) ConcurrentMap[K, V] { return create[K, V](sharding) } @@ -83,7 +83,7 @@ func (m ConcurrentMap[K, V]) Set(key K, value V) { // It is called while lock is held, therefore it MUST NOT // try to access other keys in same map, as it can lead to deadlock since // Go sync.RWLock is not reentrant -type UpsertCb[V any] func(exist bool, valueInMap V, newValue V) V +type UpsertCb[V interface{}] func(exist bool, valueInMap V, newValue V) V // Upsert Insert or Update - updates existing element or inserts a new one using UpsertCb func (m ConcurrentMap[K, V]) Upsert(key K, value V, cb UpsertCb[V]) (res V) { @@ -159,7 +159,7 @@ func (m ConcurrentMap[K, V]) Remove(key K) (err error) { // RemoveCb is a callback executed in a map.RemoveCb() call, while Lock is held // If returns true, the element will be removed from the map -type RemoveCb[K any, V any] func(key K, v V, exists bool) bool +type RemoveCb[K interface{}, V interface{}] func(key K, v V, exists bool) bool // RemoveCb locks the shard containing the key, retrieves its current value and calls the callback with those params // If callback returns true and element exists, it will remove it from the map @@ -194,7 +194,7 @@ func (m ConcurrentMap[K, V]) IsEmpty() bool { } // Tuple is used by the Iter & IterBuffered functions to wrap two variables together over a channel, -type Tuple[K comparable, V any] struct { +type Tuple[K comparable, V interface{}] struct { Key K Val V } @@ -222,7 +222,7 @@ func (m ConcurrentMap[K, V]) Clear() { // which likely takes a snapshot of `m`. // It returns once the size of each buffered channel is determined, // before all the channels are populated using goroutines. -func snapshot[K comparable, V any](m ConcurrentMap[K, V]) (chans []chan Tuple[K, V]) { +func snapshot[K comparable, V interface{}](m ConcurrentMap[K, V]) (chans []chan Tuple[K, V]) { //When you access map items before initializing. if len(m.shards) == 0 { panic(`cmap.ConcurrentMap is not initialized. Should run New() before usage.`) @@ -249,7 +249,7 @@ func snapshot[K comparable, V any](m ConcurrentMap[K, V]) (chans []chan Tuple[K, } // fanIn reads elements from channels `chans` into channel `out` -func fanIn[K comparable, V any](chans []chan Tuple[K, V], out chan Tuple[K, V]) { +func fanIn[K comparable, V interface{}](chans []chan Tuple[K, V], out chan Tuple[K, V]) { wg := sync.WaitGroup{} wg.Add(len(chans)) for _, ch := range chans { @@ -280,7 +280,7 @@ func (m ConcurrentMap[K, V]) Items() map[K]V { // maps. RLock is held for all calls for a given shard // therefore callback sess consistent view of a shard, // but not across the shards -type IterCb[K comparable, V any] func(key K, v V) +type IterCb[K comparable, V interface{}] func(key K, v V) // Callback based iterator, cheapest way to read // all elements in a map. diff --git a/datastructure/v2/cmap.go b/datastructure/v2/cmap.go new file mode 100644 index 0000000..8ca920f --- /dev/null +++ b/datastructure/v2/cmap.go @@ -0,0 +1,373 @@ +package datastructure + +import ( + "encoding/json" + "errors" + "fmt" + "hash" + "hash/fnv" + "sync" +) + +// ShardCount is the number of shards. +const ( + ShardCount = 32 + ShardCount32 uint32 = uint32(ShardCount) +) + +// Stringer is the interface implemented by any value that has a String method, +type Stringer interface { + fmt.Stringer + comparable +} + +// ConcurrentMap is a "thread" safe map of type string:Anything. +// To avoid lock bottlenecks this map is dived to several (ShardCount) map shards. +type ConcurrentMap[K comparable, V interface{}] struct { + shards []*ConcurrentMapShared[K, V] + hasher hash.Hash32 +} + +// ConcurrentMapShared is a "thread" safe string to anything map. +type ConcurrentMapShared[K comparable, V interface{}] struct { + items map[K]V + sync.RWMutex // Read Write mutex, guards access to internal map. +} + +func create[K comparable, V interface{}]() ConcurrentMap[K, V] { + m := ConcurrentMap[K, V]{ + hasher: fnv.New32(), + shards: make([]*ConcurrentMapShared[K, V], ShardCount), + } + for i := 0; i < ShardCount; i++ { + m.shards[i] = &ConcurrentMapShared[K, V]{items: make(map[K]V)} + } + return m +} + +// New creates a new concurrent map. +func New[V interface{}]() ConcurrentMap[string, V] { + return create[string, V]() +} + +// NewStringer creates a new concurrent map. +func NewStringer[K Stringer, V interface{}]() ConcurrentMap[K, V] { + return create[K, V]() +} + +// GetShard returns shard under given key +func (m ConcurrentMap[K, V]) GetShard(key K) *ConcurrentMapShared[K, V] { + hash := m.hasher.Sum32() + return m.shards[hash%ShardCount32] +} + +// MSet Sets the given value under the specified key. +func (m ConcurrentMap[K, V]) MSet(data map[K]V) { + for key, value := range data { + shard := m.GetShard(key) + shard.Lock() + shard.items[key] = value + shard.Unlock() + } +} + +// Set Sets the given value under the specified key. +func (m ConcurrentMap[K, V]) Set(key K, value V) { + // Get map shard. + shard := m.GetShard(key) + shard.Lock() + shard.items[key] = value + shard.Unlock() +} + +// UpsertCb callback to return new element to be inserted into the map +// It is called while lock is held, therefore it MUST NOT +// try to access other keys in same map, as it can lead to deadlock since +// Go sync.RWLock is not reentrant +type UpsertCb[V interface{}] func(exist bool, valueInMap V, newValue V) V + +// Upsert Insert or Update - updates existing element or inserts a new one using UpsertCb +func (m ConcurrentMap[K, V]) Upsert(key K, value V, cb UpsertCb[V]) (res V) { + shard := m.GetShard(key) + shard.Lock() + v, ok := shard.items[key] + res = cb(ok, v, value) + shard.items[key] = res + shard.Unlock() + return res +} + +// SetIfAbsent sets the given value under the specified key if no value was associated with it. +func (m ConcurrentMap[K, V]) SetIfAbsent(key K, value V) bool { + // Get map shard. + shard := m.GetShard(key) + shard.Lock() + _, ok := shard.items[key] + if !ok { + shard.items[key] = value + } + shard.Unlock() + return !ok +} + +// Get retrieves an element from map under given key. +func (m ConcurrentMap[K, V]) Get(key K) (V, bool) { + // Get shard + shard := m.GetShard(key) + shard.RLock() + // Get item from shard. + val, ok := shard.items[key] + shard.RUnlock() + return val, ok +} + +// Count returns the number of elements within the map. +func (m ConcurrentMap[K, V]) Count() int { + count := 0 + for i := 0; i < ShardCount; i++ { + shard := m.shards[i] + shard.RLock() + count += len(shard.items) + shard.RUnlock() + } + return count +} + +// Has looks up an item under specified key +func (m ConcurrentMap[K, V]) Has(key K) bool { + // Get shard + shard := m.GetShard(key) + shard.RLock() + // See if element is within shard. + _, ok := shard.items[key] + shard.RUnlock() + return ok +} + +// Remove removes an element from the map. +func (m ConcurrentMap[K, V]) Remove(key K) (err error) { + // Try to get shard. + shard := m.GetShard(key) + if shard == nil { + return errors.New("key not found") + } + + shard.Lock() + delete(shard.items, key) + shard.Unlock() + return +} + +// RemoveCb is a callback executed in a map.RemoveCb() call, while Lock is held +// If returns true, the element will be removed from the map +type RemoveCb[K interface{}, V interface{}] func(key K, v V, exists bool) bool + +// RemoveCb locks the shard containing the key, retrieves its current value and calls the callback with those params +// If callback returns true and element exists, it will remove it from the map +// Returns the value returned by the callback (even if element was not present in the map) +func (m ConcurrentMap[K, V]) RemoveCb(key K, cb RemoveCb[K, V]) bool { + // Try to get shard. + shard := m.GetShard(key) + shard.Lock() + v, ok := shard.items[key] + remove := cb(key, v, ok) + if remove && ok { + delete(shard.items, key) + } + shard.Unlock() + return remove +} + +// Pop removes an element from the map and returns it +func (m ConcurrentMap[K, V]) Pop(key K) (v V, exists bool) { + // Try to get shard. + shard := m.GetShard(key) + shard.Lock() + v, exists = shard.items[key] + delete(shard.items, key) + shard.Unlock() + return v, exists +} + +// IsEmpty checks if map is empty. +func (m ConcurrentMap[K, V]) IsEmpty() bool { + return m.Count() == 0 +} + +// Tuple is used by the Iter & IterBuffered functions to wrap two variables together over a channel, +type Tuple[K comparable, V interface{}] struct { + Key K + Val V +} + +// IterBuffered returns a buffered iterator which could be used in a for range loop. +func (m ConcurrentMap[K, V]) IterBuffered() <-chan Tuple[K, V] { + chans := snapshot(m) + total := 0 + for _, c := range chans { + total += cap(c) + } + ch := make(chan Tuple[K, V], total) + go fanIn(chans, ch) + return ch +} + +// Clear removes all items from map. +func (m ConcurrentMap[K, V]) Clear() { + for item := range m.IterBuffered() { + m.Remove(item.Key) + } +} + +// Returns a array of channels that contains elements in each shard, +// which likely takes a snapshot of `m`. +// It returns once the size of each buffered channel is determined, +// before all the channels are populated using goroutines. +func snapshot[K comparable, V interface{}](m ConcurrentMap[K, V]) (chans []chan Tuple[K, V]) { + //When you access map items before initializing. + if len(m.shards) == 0 { + panic(`cmap.ConcurrentMap is not initialized. Should run New() before usage.`) + } + chans = make([]chan Tuple[K, V], ShardCount) + wg := sync.WaitGroup{} + wg.Add(ShardCount) + // Foreach shard. + for index, shard := range m.shards { + go func(index int, shard *ConcurrentMapShared[K, V]) { + // Foreach key, value pair. + shard.RLock() + chans[index] = make(chan Tuple[K, V], len(shard.items)) + wg.Done() + for key, val := range shard.items { + chans[index] <- Tuple[K, V]{key, val} + } + shard.RUnlock() + close(chans[index]) + }(index, shard) + } + wg.Wait() + return chans +} + +// fanIn reads elements from channels `chans` into channel `out` +func fanIn[K comparable, V interface{}](chans []chan Tuple[K, V], out chan Tuple[K, V]) { + wg := sync.WaitGroup{} + wg.Add(len(chans)) + for _, ch := range chans { + go func(ch chan Tuple[K, V]) { + for t := range ch { + out <- t + } + wg.Done() + }(ch) + } + wg.Wait() + close(out) +} + +// Items returns all items as map[string]V +func (m ConcurrentMap[K, V]) Items() map[K]V { + tmp := make(map[K]V) + + // Insert items to temporary map. + for item := range m.IterBuffered() { + tmp[item.Key] = item.Val + } + + return tmp +} + +// IterCb is the iterator callbacalled for every key,value found in +// maps. RLock is held for all calls for a given shard +// therefore callback sess consistent view of a shard, +// but not across the shards +type IterCb[K comparable, V interface{}] func(key K, v V) + +// Callback based iterator, cheapest way to read +// all elements in a map. +func (m ConcurrentMap[K, V]) IterCb(fn IterCb[K, V]) { + for idx := range m.shards { + shard := (m.shards)[idx] + shard.RLock() + for key, value := range shard.items { + fn(key, value) + } + shard.RUnlock() + } +} + +// Keys returns all keys as []string +func (m ConcurrentMap[K, V]) Keys() []K { + count := m.Count() + ch := make(chan K, count) + go func() { + // Foreach shard. + wg := sync.WaitGroup{} + wg.Add(ShardCount) + for _, shard := range m.shards { + go func(shard *ConcurrentMapShared[K, V]) { + // Foreach key, value pair. + shard.RLock() + for key := range shard.items { + ch <- key + } + shard.RUnlock() + wg.Done() + }(shard) + } + wg.Wait() + close(ch) + }() + + // Generate keys + keys := make([]K, 0, count) + for k := range ch { + keys = append(keys, k) + } + return keys +} + +// MarshalJSON reviles ConcurrentMap "private" variables to json marshal. +func (m ConcurrentMap[K, V]) MarshalJSON() ([]byte, error) { + // Create a temporary map, which will hold all item spread across shards. + tmp := make(map[K]V) + + // Insert items to temporary map. + for item := range m.IterBuffered() { + tmp[item.Key] = item.Val + } + return json.Marshal(tmp) +} + +// // Returns a hash for a key. +// func strfnv32[K fmt.Stringer](key K) uint32 { +// return fnv32(key.String()) +// } + +// // Returns a hash for a string. +// func fnv32(key string) uint32 { +// hash := uint32(2166136261) +// const prime32 = uint32(16777619) +// keyLength := len(key) +// for i := 0; i < keyLength; i++ { +// hash *= prime32 +// hash ^= uint32(key[i]) +// } +// return hash +// } + +// UnmarshalJSON reverse process of Marshal. +func (m *ConcurrentMap[K, V]) UnmarshalJSON(b []byte) (err error) { + tmp := make(map[K]V) + + // Unmarshal into a single map. + if err := json.Unmarshal(b, &tmp); err != nil { + return err + } + + // foreach key,value pair in temporary map insert into our concurrent map. + for key, val := range tmp { + m.Set(key, val) + } + return nil +} diff --git a/datastructure/v3/cmap.go b/datastructure/v3/cmap.go new file mode 100644 index 0000000..aa8524b --- /dev/null +++ b/datastructure/v3/cmap.go @@ -0,0 +1,198 @@ +package v3 + +import ( + "hash" + "hash/fnv" + "sync" + + "github.com/hyp3rd/hypercache/models" +) + +const ( + // ShardCount is the number of shards used by the map. + ShardCount = 32 + // ShardCount32 is the number of shards used by the map pre-casted to uint32 to performace issues. + ShardCount32 uint32 = uint32(ShardCount) +) + +var ( + // hasherSyncPool is a pool of hashers. + hasherSyncPool = sync.Pool{ + New: func() interface{} { + return fnv.New32a() + }, + } +) + +// ConcurrentMap is a "thread" safe map of type string:*models.Item. +// To avoid lock bottlenecks this map is dived to several (ShardCount) map shards. +type ConcurrentMap struct { + shards []*ConcurrentMapShard + hasher hash.Hash32 +} + +// ConcurrentMapShared is a "thread" safe string to `*models.Item`. +type ConcurrentMapShard struct { + items map[string]*models.Item + sync.RWMutex +} + +// New creates a new concurrent map. +func New() ConcurrentMap { + h := hasherSyncPool.Get().(hash.Hash32) + defer hasherSyncPool.Put(h) + return ConcurrentMap{ + shards: create(), + hasher: h, + } +} + +func create() []*ConcurrentMapShard { + shards := make([]*ConcurrentMapShard, ShardCount) + for i := 0; i < ShardCount; i++ { + shards[i] = &ConcurrentMapShard{items: make(map[string]*models.Item)} + } + return shards +} + +// GetShard returns shard under given key +func (m *ConcurrentMap) GetShard(key string) *ConcurrentMapShard { + // Calculate the shard index using a bitwise AND operation + shardIndex := m.hasher.Sum32() & (ShardCount32 - 1) + return m.shards[shardIndex] +} + +// Set sets the given value under the specified key. +func (m *ConcurrentMap) Set(key string, value *models.Item) { + shard := m.GetShard(key) + shard.Lock() + shard.items[key] = value + shard.Unlock() +} + +// Get retrieves an element from map under given key. +func (m *ConcurrentMap) Get(key string) (*models.Item, bool) { + // Get shard + shard := m.GetShard(key) + shard.RLock() + // Get item from shard. + item, ok := shard.items[key] + shard.RUnlock() + return item, ok +} + +// Has checks if key is present in the map. +func (m *ConcurrentMap) Has(key string) bool { + // Get shard + shard := m.GetShard(key) + shard.RLock() + // Get item from shard. + _, ok := shard.items[key] + shard.RUnlock() + return ok +} + +// Pop removes an element from the map and returns it. +func (m *ConcurrentMap) Pop(key string) (*models.Item, bool) { + shard := m.GetShard(key) + shard.Lock() + item, ok := shard.items[key] + if !ok { + shard.Unlock() + return nil, false + } + delete(shard.items, key) + shard.Unlock() + return item, ok +} + +// Tuple is used by the IterBuffered functions to wrap two variables together over a channel, +type Tuple struct { + Key string + Val models.Item +} + +// IterBuffered returns a buffered iterator which could be used in a for range loop. +func (m ConcurrentMap) IterBuffered() <-chan Tuple { + chans := snapshot(m) + total := 0 + for _, c := range chans { + total += cap(c) + } + ch := make(chan Tuple, total) + go fanIn(chans, ch) + return ch +} + +// Returns a array of channels that contains elements in each shard, +// which likely takes a snapshot of `m`. +// It returns once the size of each buffered channel is determined, +// before all the channels are populated using goroutines. +func snapshot(m ConcurrentMap) (chans []chan Tuple) { + //When you access map items before initializing. + if len(m.shards) == 0 { + panic(`cmap.ConcurrentMap is not initialized. Should run New() before usage.`) + } + chans = make([]chan Tuple, ShardCount) + wg := sync.WaitGroup{} + wg.Add(ShardCount) + // Foreach shard. + for index, shard := range m.shards { + go func(index int, shard *ConcurrentMapShard) { + // Foreach key, value pair. + shard.RLock() + chans[index] = make(chan Tuple, len(shard.items)) + wg.Done() + for key, val := range shard.items { + chans[index] <- Tuple{key, *val} + } + shard.RUnlock() + close(chans[index]) + }(index, shard) + } + wg.Wait() + return chans +} + +// fanIn reads elements from channels `chans` into channel `out` +func fanIn(chans []chan Tuple, out chan Tuple) { + wg := sync.WaitGroup{} + wg.Add(len(chans)) + for _, ch := range chans { + go func(ch chan Tuple) { + for t := range ch { + out <- t + } + wg.Done() + }(ch) + } + wg.Wait() + close(out) +} + +// Remove removes the value under the specified key. +func (m *ConcurrentMap) Remove(key string) { + // Get map shard. + shard := m.GetShard(key) + shard.Lock() + delete(shard.items, key) + shard.Unlock() +} + +// Clear removes all items from map. +func (m ConcurrentMap) Clear() { + for item := range m.IterBuffered() { + m.Remove(item.Key) + } +} + +// Count returns the number of items in the map. +func (m *ConcurrentMap) Count() int { + count := 0 + for _, shard := range m.shards { + shard.RLock() + count += len(shard.items) + shard.RUnlock() + } + return count +} diff --git a/examples/list/list.go b/examples/list/list.go index 53d4de9..019aee6 100644 --- a/examples/list/list.go +++ b/examples/list/list.go @@ -23,7 +23,7 @@ func main() { defer hyperCache.Stop() // Add 100 items to the cache - for i := 0; i < 400; i++ { + for i := 0; i < 500; i++ { key := fmt.Sprintf("key%d", i) val := fmt.Sprintf("val%d", i) From 90a2f7a73823de9c08b11fe9508d7ff06cb95376 Mon Sep 17 00:00:00 2001 From: Francesco Cosentino Date: Wed, 25 Jan 2023 20:02:01 +0100 Subject: [PATCH 5/9] better map and clean up --- backend/inmemory.go.bak3 | 174 ------------------ datastructure/v2/cmap.go | 373 --------------------------------------- datastructure/v3/cmap.go | 16 +- 3 files changed, 4 insertions(+), 559 deletions(-) delete mode 100644 backend/inmemory.go.bak3 delete mode 100644 datastructure/v2/cmap.go diff --git a/backend/inmemory.go.bak3 b/backend/inmemory.go.bak3 deleted file mode 100644 index 98113ba..0000000 --- a/backend/inmemory.go.bak3 +++ /dev/null @@ -1,174 +0,0 @@ -package backend - -import ( - "fmt" - "sort" - "sync" - - datastructure "github.com/hyp3rd/hypercache/datastructure/v2" - "github.com/hyp3rd/hypercache/errors" - "github.com/hyp3rd/hypercache/models" - "github.com/hyp3rd/hypercache/types" -) - -// InMemory is a cache backend that stores the items in memory, leveraging a custom `ConcurrentMap`. -type InMemory struct { - items datastructure.ConcurrentMap[string, *models.Item] // map to store the items in the cache - capacity int // capacity of the cache, limits the number of items that can be stored in the cache - maxCacheSize int // maxCacheSize instructs the cache not allocate more memory than this limit, value in MB, 0 means no limit - memoryAllocation int // memoryAllocation is the current memory allocation of the cache, value in bytes - mutex sync.RWMutex // mutex to protect the cache from concurrent access - SortFilters // filters applied when listing the items in the cache -} - -// NewInMemory creates a new in-memory cache with the given options. -func NewInMemory[T InMemory](opts ...Option[InMemory]) (backend IInMemory[T], err error) { - InMemory := &InMemory{ - items: datastructure.New[*models.Item](), - } - // Apply the backend options - ApplyOptions(InMemory, opts...) - // Check if the `capacity` is valid - if InMemory.capacity < 0 { - return nil, errors.ErrInvalidCapacity - } - // Check if the `maxCacheSize` is valid - if InMemory.maxCacheSize < 0 { - return nil, errors.ErrInvalidMaxCacheSize - } - - return InMemory, nil -} - -// SetCapacity sets the capacity of the cache. -func (cacheBackend *InMemory) SetCapacity(capacity int) { - if capacity < 0 { - return - } - cacheBackend.capacity = capacity -} - -// Capacity returns the capacity of the cacheBackend. -func (cacheBackend *InMemory) Capacity() int { - return cacheBackend.capacity -} - -// Count returns the number of items in the cache. -func (cacheBackend *InMemory) Count() int { - return cacheBackend.items.Count() -} - -// Size returns the number of items in the cacheBackend. -func (cacheBackend *InMemory) Size() int { - return cacheBackend.memoryAllocation -} - -// MaxCacheSize returns the maximum size in bytes of the cacheBackend. -func (cacheBackend *InMemory) MaxCacheSize() int { - return cacheBackend.maxCacheSize -} - -// Get retrieves the item with the given key from the cacheBackend. If the item is not found, it returns nil. -func (cacheBackend *InMemory) Get(key string) (item *models.Item, ok bool) { - item, ok = cacheBackend.items.Get(key) - if !ok { - return nil, false - } - // return the item - return item, true -} - -// Set adds a Item to the cache. -func (cacheBackend *InMemory) Set(item *models.Item) error { - // Check for invalid key, value, or duration - if err := item.Valid(); err != nil { - models.ItemPool.Put(item) - return err - } - - cacheBackend.mutex.Lock() - defer cacheBackend.mutex.Unlock() - // Set the size of the item - err := item.SetSize() - if err != nil { - return err - } - - // check if adding this item will exceed the maxCacheSize - cacheBackend.memoryAllocation = cacheBackend.memoryAllocation + item.Size - if cacheBackend.maxCacheSize > 0 && cacheBackend.memoryAllocation > cacheBackend.maxCacheSize { - return errors.ErrCacheFull - } - - cacheBackend.items.Set(item.Key, item) - return nil -} - -// List returns a list of all items in the cache filtered and ordered by the given options -func (cacheBackend *InMemory) List(options ...FilterOption[InMemory]) ([]*models.Item, error) { - // Apply the filter options - ApplyFilterOptions(cacheBackend, options...) - - items := make([]*models.Item, 0, cacheBackend.items.Count()) - wg := sync.WaitGroup{} - wg.Add(cacheBackend.items.Count()) - for item := range cacheBackend.items.IterBuffered() { - go func(item datastructure.Tuple[string, *models.Item]) { - defer wg.Done() - if cacheBackend.FilterFunc == nil || cacheBackend.FilterFunc(item.Val) { - items = append(items, item.Val) - } - }(item) - } - wg.Wait() - - if cacheBackend.SortBy == "" { - return items, nil - } - - var sorter sort.Interface - switch cacheBackend.SortBy { - case types.SortByKey.String(): - sorter = &itemSorterByKey{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 -} - -// Remove removes items with the given key from the cacheBackend. If an item is not found, it does nothing. -func (cacheBackend *InMemory) Remove(keys ...string) (err error) { - //TODO: determine if handling the error or not - // var ok bool - // item := models.ItemPool.Get().(*models.Item) - // defer models.ItemPool.Put(item) - for _, key := range keys { - item, ok := cacheBackend.items.Get(key) - if ok { - // remove the item from the cacheBackend and update the memory allocation - cacheBackend.memoryAllocation = cacheBackend.memoryAllocation - item.Size - cacheBackend.items.Remove(key) - } - } - return -} - -// Clear removes all items from the cacheBackend. -func (cacheBackend *InMemory) Clear() { - // clear the cacheBackend - cacheBackend.items.Clear() - // reset the memory allocation - cacheBackend.memoryAllocation = 0 -} diff --git a/datastructure/v2/cmap.go b/datastructure/v2/cmap.go deleted file mode 100644 index 8ca920f..0000000 --- a/datastructure/v2/cmap.go +++ /dev/null @@ -1,373 +0,0 @@ -package datastructure - -import ( - "encoding/json" - "errors" - "fmt" - "hash" - "hash/fnv" - "sync" -) - -// ShardCount is the number of shards. -const ( - ShardCount = 32 - ShardCount32 uint32 = uint32(ShardCount) -) - -// Stringer is the interface implemented by any value that has a String method, -type Stringer interface { - fmt.Stringer - comparable -} - -// ConcurrentMap is a "thread" safe map of type string:Anything. -// To avoid lock bottlenecks this map is dived to several (ShardCount) map shards. -type ConcurrentMap[K comparable, V interface{}] struct { - shards []*ConcurrentMapShared[K, V] - hasher hash.Hash32 -} - -// ConcurrentMapShared is a "thread" safe string to anything map. -type ConcurrentMapShared[K comparable, V interface{}] struct { - items map[K]V - sync.RWMutex // Read Write mutex, guards access to internal map. -} - -func create[K comparable, V interface{}]() ConcurrentMap[K, V] { - m := ConcurrentMap[K, V]{ - hasher: fnv.New32(), - shards: make([]*ConcurrentMapShared[K, V], ShardCount), - } - for i := 0; i < ShardCount; i++ { - m.shards[i] = &ConcurrentMapShared[K, V]{items: make(map[K]V)} - } - return m -} - -// New creates a new concurrent map. -func New[V interface{}]() ConcurrentMap[string, V] { - return create[string, V]() -} - -// NewStringer creates a new concurrent map. -func NewStringer[K Stringer, V interface{}]() ConcurrentMap[K, V] { - return create[K, V]() -} - -// GetShard returns shard under given key -func (m ConcurrentMap[K, V]) GetShard(key K) *ConcurrentMapShared[K, V] { - hash := m.hasher.Sum32() - return m.shards[hash%ShardCount32] -} - -// MSet Sets the given value under the specified key. -func (m ConcurrentMap[K, V]) MSet(data map[K]V) { - for key, value := range data { - shard := m.GetShard(key) - shard.Lock() - shard.items[key] = value - shard.Unlock() - } -} - -// Set Sets the given value under the specified key. -func (m ConcurrentMap[K, V]) Set(key K, value V) { - // Get map shard. - shard := m.GetShard(key) - shard.Lock() - shard.items[key] = value - shard.Unlock() -} - -// UpsertCb callback to return new element to be inserted into the map -// It is called while lock is held, therefore it MUST NOT -// try to access other keys in same map, as it can lead to deadlock since -// Go sync.RWLock is not reentrant -type UpsertCb[V interface{}] func(exist bool, valueInMap V, newValue V) V - -// Upsert Insert or Update - updates existing element or inserts a new one using UpsertCb -func (m ConcurrentMap[K, V]) Upsert(key K, value V, cb UpsertCb[V]) (res V) { - shard := m.GetShard(key) - shard.Lock() - v, ok := shard.items[key] - res = cb(ok, v, value) - shard.items[key] = res - shard.Unlock() - return res -} - -// SetIfAbsent sets the given value under the specified key if no value was associated with it. -func (m ConcurrentMap[K, V]) SetIfAbsent(key K, value V) bool { - // Get map shard. - shard := m.GetShard(key) - shard.Lock() - _, ok := shard.items[key] - if !ok { - shard.items[key] = value - } - shard.Unlock() - return !ok -} - -// Get retrieves an element from map under given key. -func (m ConcurrentMap[K, V]) Get(key K) (V, bool) { - // Get shard - shard := m.GetShard(key) - shard.RLock() - // Get item from shard. - val, ok := shard.items[key] - shard.RUnlock() - return val, ok -} - -// Count returns the number of elements within the map. -func (m ConcurrentMap[K, V]) Count() int { - count := 0 - for i := 0; i < ShardCount; i++ { - shard := m.shards[i] - shard.RLock() - count += len(shard.items) - shard.RUnlock() - } - return count -} - -// Has looks up an item under specified key -func (m ConcurrentMap[K, V]) Has(key K) bool { - // Get shard - shard := m.GetShard(key) - shard.RLock() - // See if element is within shard. - _, ok := shard.items[key] - shard.RUnlock() - return ok -} - -// Remove removes an element from the map. -func (m ConcurrentMap[K, V]) Remove(key K) (err error) { - // Try to get shard. - shard := m.GetShard(key) - if shard == nil { - return errors.New("key not found") - } - - shard.Lock() - delete(shard.items, key) - shard.Unlock() - return -} - -// RemoveCb is a callback executed in a map.RemoveCb() call, while Lock is held -// If returns true, the element will be removed from the map -type RemoveCb[K interface{}, V interface{}] func(key K, v V, exists bool) bool - -// RemoveCb locks the shard containing the key, retrieves its current value and calls the callback with those params -// If callback returns true and element exists, it will remove it from the map -// Returns the value returned by the callback (even if element was not present in the map) -func (m ConcurrentMap[K, V]) RemoveCb(key K, cb RemoveCb[K, V]) bool { - // Try to get shard. - shard := m.GetShard(key) - shard.Lock() - v, ok := shard.items[key] - remove := cb(key, v, ok) - if remove && ok { - delete(shard.items, key) - } - shard.Unlock() - return remove -} - -// Pop removes an element from the map and returns it -func (m ConcurrentMap[K, V]) Pop(key K) (v V, exists bool) { - // Try to get shard. - shard := m.GetShard(key) - shard.Lock() - v, exists = shard.items[key] - delete(shard.items, key) - shard.Unlock() - return v, exists -} - -// IsEmpty checks if map is empty. -func (m ConcurrentMap[K, V]) IsEmpty() bool { - return m.Count() == 0 -} - -// Tuple is used by the Iter & IterBuffered functions to wrap two variables together over a channel, -type Tuple[K comparable, V interface{}] struct { - Key K - Val V -} - -// IterBuffered returns a buffered iterator which could be used in a for range loop. -func (m ConcurrentMap[K, V]) IterBuffered() <-chan Tuple[K, V] { - chans := snapshot(m) - total := 0 - for _, c := range chans { - total += cap(c) - } - ch := make(chan Tuple[K, V], total) - go fanIn(chans, ch) - return ch -} - -// Clear removes all items from map. -func (m ConcurrentMap[K, V]) Clear() { - for item := range m.IterBuffered() { - m.Remove(item.Key) - } -} - -// Returns a array of channels that contains elements in each shard, -// which likely takes a snapshot of `m`. -// It returns once the size of each buffered channel is determined, -// before all the channels are populated using goroutines. -func snapshot[K comparable, V interface{}](m ConcurrentMap[K, V]) (chans []chan Tuple[K, V]) { - //When you access map items before initializing. - if len(m.shards) == 0 { - panic(`cmap.ConcurrentMap is not initialized. Should run New() before usage.`) - } - chans = make([]chan Tuple[K, V], ShardCount) - wg := sync.WaitGroup{} - wg.Add(ShardCount) - // Foreach shard. - for index, shard := range m.shards { - go func(index int, shard *ConcurrentMapShared[K, V]) { - // Foreach key, value pair. - shard.RLock() - chans[index] = make(chan Tuple[K, V], len(shard.items)) - wg.Done() - for key, val := range shard.items { - chans[index] <- Tuple[K, V]{key, val} - } - shard.RUnlock() - close(chans[index]) - }(index, shard) - } - wg.Wait() - return chans -} - -// fanIn reads elements from channels `chans` into channel `out` -func fanIn[K comparable, V interface{}](chans []chan Tuple[K, V], out chan Tuple[K, V]) { - wg := sync.WaitGroup{} - wg.Add(len(chans)) - for _, ch := range chans { - go func(ch chan Tuple[K, V]) { - for t := range ch { - out <- t - } - wg.Done() - }(ch) - } - wg.Wait() - close(out) -} - -// Items returns all items as map[string]V -func (m ConcurrentMap[K, V]) Items() map[K]V { - tmp := make(map[K]V) - - // Insert items to temporary map. - for item := range m.IterBuffered() { - tmp[item.Key] = item.Val - } - - return tmp -} - -// IterCb is the iterator callbacalled for every key,value found in -// maps. RLock is held for all calls for a given shard -// therefore callback sess consistent view of a shard, -// but not across the shards -type IterCb[K comparable, V interface{}] func(key K, v V) - -// Callback based iterator, cheapest way to read -// all elements in a map. -func (m ConcurrentMap[K, V]) IterCb(fn IterCb[K, V]) { - for idx := range m.shards { - shard := (m.shards)[idx] - shard.RLock() - for key, value := range shard.items { - fn(key, value) - } - shard.RUnlock() - } -} - -// Keys returns all keys as []string -func (m ConcurrentMap[K, V]) Keys() []K { - count := m.Count() - ch := make(chan K, count) - go func() { - // Foreach shard. - wg := sync.WaitGroup{} - wg.Add(ShardCount) - for _, shard := range m.shards { - go func(shard *ConcurrentMapShared[K, V]) { - // Foreach key, value pair. - shard.RLock() - for key := range shard.items { - ch <- key - } - shard.RUnlock() - wg.Done() - }(shard) - } - wg.Wait() - close(ch) - }() - - // Generate keys - keys := make([]K, 0, count) - for k := range ch { - keys = append(keys, k) - } - return keys -} - -// MarshalJSON reviles ConcurrentMap "private" variables to json marshal. -func (m ConcurrentMap[K, V]) MarshalJSON() ([]byte, error) { - // Create a temporary map, which will hold all item spread across shards. - tmp := make(map[K]V) - - // Insert items to temporary map. - for item := range m.IterBuffered() { - tmp[item.Key] = item.Val - } - return json.Marshal(tmp) -} - -// // Returns a hash for a key. -// func strfnv32[K fmt.Stringer](key K) uint32 { -// return fnv32(key.String()) -// } - -// // Returns a hash for a string. -// func fnv32(key string) uint32 { -// hash := uint32(2166136261) -// const prime32 = uint32(16777619) -// keyLength := len(key) -// for i := 0; i < keyLength; i++ { -// hash *= prime32 -// hash ^= uint32(key[i]) -// } -// return hash -// } - -// UnmarshalJSON reverse process of Marshal. -func (m *ConcurrentMap[K, V]) UnmarshalJSON(b []byte) (err error) { - tmp := make(map[K]V) - - // Unmarshal into a single map. - if err := json.Unmarshal(b, &tmp); err != nil { - return err - } - - // foreach key,value pair in temporary map insert into our concurrent map. - for key, val := range tmp { - m.Set(key, val) - } - return nil -} diff --git a/datastructure/v3/cmap.go b/datastructure/v3/cmap.go index aa8524b..6d71fcb 100644 --- a/datastructure/v3/cmap.go +++ b/datastructure/v3/cmap.go @@ -15,15 +15,6 @@ const ( ShardCount32 uint32 = uint32(ShardCount) ) -var ( - // hasherSyncPool is a pool of hashers. - hasherSyncPool = sync.Pool{ - New: func() interface{} { - return fnv.New32a() - }, - } -) - // ConcurrentMap is a "thread" safe map of type string:*models.Item. // To avoid lock bottlenecks this map is dived to several (ShardCount) map shards. type ConcurrentMap struct { @@ -39,11 +30,12 @@ type ConcurrentMapShard struct { // New creates a new concurrent map. func New() ConcurrentMap { - h := hasherSyncPool.Get().(hash.Hash32) - defer hasherSyncPool.Put(h) + // h := hasherSyncPool.Get().(hash.Hash32) + // defer hasherSyncPool.Put(h) return ConcurrentMap{ shards: create(), - hasher: h, + // hasher: h, + hasher: fnv.New32a(), } } From 26b8bf3ec77b524b5f94bd8777f6aa4f01313c57 Mon Sep 17 00:00:00 2001 From: Francesco Cosentino Date: Wed, 25 Jan 2023 20:03:31 +0100 Subject: [PATCH 6/9] clean up --- stats/histogramcollector.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/stats/histogramcollector.go b/stats/histogramcollector.go index e11f0bc..9638182 100644 --- a/stats/histogramcollector.go +++ b/stats/histogramcollector.go @@ -23,7 +23,6 @@ func NewHistogramStatsCollector() *HistogramStatsCollector { // Incr increments the count of a statistic by the given value. func (c *HistogramStatsCollector) Incr(stat types.Stat, value int64) { - // Lock the cache's mutex to ensure thread-safety c.mu.Lock() defer c.mu.Unlock() c.stats[stat.String()] = append(c.stats[stat.String()], value) @@ -31,7 +30,6 @@ func (c *HistogramStatsCollector) Incr(stat types.Stat, value int64) { // Decr decrements the count of a statistic by the given value. func (c *HistogramStatsCollector) Decr(stat types.Stat, value int64) { - // Lock the cache's mutex to ensure thread-safety c.mu.Lock() defer c.mu.Unlock() c.stats[stat.String()] = append(c.stats[stat.String()], -value) @@ -39,7 +37,7 @@ func (c *HistogramStatsCollector) Decr(stat types.Stat, value int64) { // Timing records the time it took for an event to occur. func (c *HistogramStatsCollector) Timing(stat types.Stat, value int64) { - // Lock the cache's mutex to ensure thread-safety + c.mu.Lock() defer c.mu.Unlock() c.stats[stat.String()] = append(c.stats[stat.String()], value) @@ -47,7 +45,6 @@ func (c *HistogramStatsCollector) Timing(stat types.Stat, value int64) { // Gauge records the current value of a statistic. func (c *HistogramStatsCollector) Gauge(stat types.Stat, value int64) { - // Lock the cache's mutex to ensure thread-safety c.mu.Lock() defer c.mu.Unlock() c.stats[stat.String()] = append(c.stats[stat.String()], value) @@ -55,7 +52,6 @@ func (c *HistogramStatsCollector) Gauge(stat types.Stat, value int64) { // Histogram records the statistical distribution of a set of values. func (c *HistogramStatsCollector) Histogram(stat types.Stat, value int64) { - // Lock the cache's mutex to ensure thread-safety c.mu.Lock() defer c.mu.Unlock() c.stats[stat.String()] = append(c.stats[stat.String()], value) From 49188df41f7ae3373ddeb9ea62e65c0210929616 Mon Sep 17 00:00:00 2001 From: Francesco Cosentino Date: Wed, 25 Jan 2023 23:24:40 +0100 Subject: [PATCH 7/9] implemented size handling --- backend/backend.go | 2 - backend/inmemory.go | 44 ++-------- backend/options.go | 22 ----- backend/redis.go | 28 +----- config.go | 13 +++ examples/clear/clear.go | 15 +++- examples/redis/redis.go | 4 +- examples/size/size.go | 4 +- go.mod | 1 + go.sum | 2 + hypercache.go | 132 +++++++++++++++------------- middleware/logging.go | 13 ++- middleware/stats.go | 11 ++- models/item.go | 71 +++++++-------- service.go | 6 +- tests/hypercache_get_or_set_test.go | 2 +- tests/hypercache_set_test.go | 2 +- 17 files changed, 168 insertions(+), 204 deletions(-) diff --git a/backend/backend.go b/backend/backend.go index b434f74..054333d 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -41,8 +41,6 @@ type IBackend[T IBackendConstrain] interface { Capacity() int // SetCapacity sets the maximum number of items that can be stored in the cache. SetCapacity(capacity int) - // Size returns the size in bytes of items currently stored in the cache. - Size() int // Count returns the number of items currently stored in the cache. Count() int // Remove deletes the item with the given key from the cache. diff --git a/backend/inmemory.go b/backend/inmemory.go index 2d8b658..03626e4 100644 --- a/backend/inmemory.go +++ b/backend/inmemory.go @@ -14,12 +14,10 @@ import ( // InMemory is a cache backend that stores the items in memory, leveraging a custom `ConcurrentMap`. type InMemory struct { // items datastructure.ConcurrentMap[string, *models.Item] // map to store the items in the cache - items datastructure.ConcurrentMap // map to store the items in the cache - capacity int // capacity of the cache, limits the number of items that can be stored in the cache - maxCacheSize int // maxCacheSize instructs the cache not allocate more memory than this limit, value in MB, 0 means no limit - memoryAllocation int // memoryAllocation is the current memory allocation of the cache, value in bytes - mutex sync.RWMutex // mutex to protect the cache from concurrent access - SortFilters // filters applied when listing the items in the cache + items datastructure.ConcurrentMap // map to store the items in the cache + capacity int // capacity of the cache, limits the number of items that can be stored in the cache + mutex sync.RWMutex // mutex to protect the cache from concurrent access + SortFilters // filters applied when listing the items in the cache } // NewInMemory creates a new in-memory cache with the given options. @@ -34,10 +32,6 @@ func NewInMemory[T InMemory](opts ...Option[InMemory]) (backend IInMemory[T], er if InMemory.capacity < 0 { return nil, errors.ErrInvalidCapacity } - // Check if the `maxCacheSize` is valid - if InMemory.maxCacheSize < 0 { - return nil, errors.ErrInvalidMaxCacheSize - } return InMemory, nil } @@ -60,16 +54,6 @@ func (cacheBackend *InMemory) Count() int { return cacheBackend.items.Count() } -// Size returns the number of items in the cacheBackend. -func (cacheBackend *InMemory) Size() int { - return cacheBackend.memoryAllocation -} - -// MaxCacheSize returns the maximum size in bytes of the cacheBackend. -func (cacheBackend *InMemory) MaxCacheSize() int { - return cacheBackend.maxCacheSize -} - // Get retrieves the item with the given key from the cacheBackend. If the item is not found, it returns nil. func (cacheBackend *InMemory) Get(key string) (item *models.Item, ok bool) { item, ok = cacheBackend.items.Get(key) @@ -90,17 +74,6 @@ func (cacheBackend *InMemory) Set(item *models.Item) error { cacheBackend.mutex.Lock() defer cacheBackend.mutex.Unlock() - // Set the size of the item - err := item.SetSize() - if err != nil { - return err - } - - // check if adding this item will exceed the maxCacheSize - cacheBackend.memoryAllocation = cacheBackend.memoryAllocation + item.Size - if cacheBackend.maxCacheSize > 0 && cacheBackend.memoryAllocation > cacheBackend.maxCacheSize { - return errors.ErrCacheFull - } cacheBackend.items.Set(item.Key, item) return nil @@ -158,12 +131,7 @@ func (cacheBackend *InMemory) Remove(keys ...string) (err error) { // item := models.ItemPool.Get().(*models.Item) // defer models.ItemPool.Put(item) for _, key := range keys { - item, ok := cacheBackend.items.Get(key) - if ok { - // remove the item from the cacheBackend and update the memory allocation - cacheBackend.memoryAllocation = cacheBackend.memoryAllocation - item.Size - cacheBackend.items.Remove(key) - } + cacheBackend.items.Remove(key) } return } @@ -172,6 +140,4 @@ func (cacheBackend *InMemory) Remove(keys ...string) (err error) { func (cacheBackend *InMemory) Clear() { // clear the cacheBackend cacheBackend.items.Clear() - // reset the memory allocation - cacheBackend.memoryAllocation = 0 } diff --git a/backend/options.go b/backend/options.go index 25f1352..b0ab041 100644 --- a/backend/options.go +++ b/backend/options.go @@ -57,8 +57,6 @@ func (rb *Redis) setFilterFunc(filterFunc FilterFunc) { type iConfigurableBackend interface { // setCapacity sets the capacity of the cache. setCapacity(capacity int) - // setMaxCacheSize sets the maximum size of the cache. - setMaxCacheSize(maxCacheSize int) } // setCapacity sets the `Capacity` field of the `InMemory` backend. @@ -71,15 +69,6 @@ func (rb *Redis) setCapacity(capacity int) { rb.capacity = capacity } -// setMaxCacheSize sets the `maxCacheSize` field of the `InMemory` backend. -func (inm *InMemory) setMaxCacheSize(maxCacheSize int) { - inm.maxCacheSize = maxCacheSize -} - -func (rb *Redis) setMaxCacheSize(maxCacheSize int) { - rb.maxCacheSize = maxCacheSize -} - // Option is a function type that can be used to configure the `HyperCache` struct. type Option[T IBackendConstrain] func(*T) @@ -90,17 +79,6 @@ func ApplyOptions[T IBackendConstrain](backend *T, options ...Option[T]) { } } -// WithMaxCacheSize is an option that sets the maximum size of the cache. -// The maximum size of the cache is the maximum number of items that can be stored in the cache. -// If the maximum size of the cache is reached, the least recently used item will be evicted from the cache. -func WithMaxCacheSize[T IBackendConstrain](maxCacheSize int) Option[T] { - return func(a *T) { - if configurable, ok := any(a).(iConfigurableBackend); ok { - configurable.setMaxCacheSize(maxCacheSize) - } - } -} - // WithCapacity is an option that sets the capacity of the cache. func WithCapacity[T IBackendConstrain](capacity int) Option[T] { return func(a *T) { diff --git a/backend/redis.go b/backend/redis.go index fd6084e..778e02d 100644 --- a/backend/redis.go +++ b/backend/redis.go @@ -14,11 +14,9 @@ import ( // 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 - maxCacheSize int // maxCacheSize is the maximum number of items that can be stored in the cache - memoryAllocation int // memoryAllocation is the current memory allocation of the cache, value in bytes + 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 @@ -38,10 +36,6 @@ func NewRedisBackend[T Redis](redisOptions ...Option[Redis]) (backend IRedisBack if rb.capacity < 0 { return nil, errors.ErrInvalidCapacity } - // Check if the `maxCacheSize` is valid - if rb.maxCacheSize < 0 { - return nil, errors.ErrInvalidMaxCacheSize - } // Check if the `keysSetName` is empty if rb.keysSetName == "" { rb.keysSetName = "hypercache" @@ -79,16 +73,6 @@ func (cacheBackend *Redis) Count() int { return int(count) } -// Size returns the number of items in the cacheBackend. -func (cacheBackend *Redis) Size() int { - return cacheBackend.memoryAllocation -} - -// MaxCacheSize returns the maximum size in bytes of the cacheBackend. -func (cacheBackend *Redis) MaxCacheSize() int { - return cacheBackend.maxCacheSize -} - // Get retrieves the Item with the given key from the cacheBackend. If the item is not found, it returns nil. func (cacheBackend *Redis) Get(key string) (item *models.Item, ok bool) { // pipe := cacheBackend.rdb.Conn().Pipeline() @@ -141,12 +125,6 @@ func (cacheBackend *Redis) Set(item *models.Item) error { return err } - // check if adding this item will exceed the maxCacheSize - cacheBackend.memoryAllocation = cacheBackend.memoryAllocation + item.Size - if cacheBackend.maxCacheSize > 0 && cacheBackend.memoryAllocation > cacheBackend.maxCacheSize { - return errors.ErrCacheFull - } - // Serialize the item data, err := cacheBackend.Serializer.Marshal(item) if err != nil { diff --git a/config.go b/config.go index 1e824a6..615dbdc 100644 --- a/config.go +++ b/config.go @@ -48,6 +48,19 @@ func ApplyHyperCacheOptions[T backend.IBackendConstrain](cache *HyperCache[T], o } } +// WithMaxCacheSize is an option that sets the maximum size of the cache. +// The maximum size of the cache is the maximum number of items that can be stored in the cache. +// If the maximum size of the cache is reached, the least recently used item will be evicted from the cache. +func WithMaxCacheSize[T backend.IBackendConstrain](maxCacheSize int64) Option[T] { + return func(cache *HyperCache[T]) { + // If the max cache size is less than 0, set it to 0. + if maxCacheSize < 0 { + maxCacheSize = 0 + } + cache.maxCacheSize = maxCacheSize + } +} + // WithEvictionAlgorithm is an option that sets the eviction algorithm name field of the `HyperCache` struct. // The eviction algorithm name determines which eviction algorithm will be used to evict items from the cache. // The eviction algorithm name must be one of the following: diff --git a/examples/clear/clear.go b/examples/clear/clear.go index 57d85e3..b55c714 100644 --- a/examples/clear/clear.go +++ b/examples/clear/clear.go @@ -16,15 +16,22 @@ func main() { // Stop the cache when the program exits defer cache.Stop() - fmt.Println("adding 10000 items to cache") + fmt.Println("adding 100000 items to cache") for i := 0; i < 100000; i++ { - cache.Set(fmt.Sprintf("key%d", i), "value", 0) + cache.Set(fmt.Sprintf("key%d", i), fmt.Sprintf("value%d", i), 0) + } + + item, ok := cache.Get("key100") + if ok { + fmt.Println("key100", item) } fmt.Println("capacity", cache.Capacity()) - fmt.Println("size", cache.Size()) + fmt.Println("count", cache.Count()) + fmt.Println("allocation", cache.Allocation()) fmt.Println("clearing cache") cache.Clear() fmt.Println("capacity", cache.Capacity()) - fmt.Println("size", cache.Size()) + fmt.Println("count", cache.Count()) + fmt.Println("allocation", cache.Allocation()) } diff --git a/examples/redis/redis.go b/examples/redis/redis.go index 2686926..f4bfc02 100644 --- a/examples/redis/redis.go +++ b/examples/redis/redis.go @@ -45,7 +45,7 @@ func main() { } } - fmt.Println("size", hyperCache.Size()) + fmt.Println("count", hyperCache.Count()) fmt.Println("capacity", hyperCache.Capacity()) allItems, err := hyperCache.List( @@ -66,7 +66,7 @@ func main() { fmt.Println(item.Key, item.Value) } - fmt.Println("size", hyperCache.Size()) + fmt.Println("count", hyperCache.Count()) fmt.Println("capacity", hyperCache.Capacity()) time.Sleep(time.Second * 10) diff --git a/examples/size/size.go b/examples/size/size.go index a789b3f..569294d 100644 --- a/examples/size/size.go +++ b/examples/size/size.go @@ -14,11 +14,11 @@ func main() { config.HyperCacheOptions = []hypercache.Option[backend.InMemory]{ hypercache.WithEvictionInterval[backend.InMemory](0), hypercache.WithEvictionAlgorithm[backend.InMemory]("cawolfu"), + hypercache.WithMaxCacheSize[backend.InMemory](37326), } config.InMemoryOptions = []backend.Option[backend.InMemory]{ backend.WithCapacity[backend.InMemory](100000), - backend.WithMaxCacheSize[backend.InMemory](7326), } // Create a new HyperCache with a capacity of 10 @@ -597,6 +597,8 @@ func main() { } else { fmt.Println("key not found") } + fmt.Println(cache.Count()) + fmt.Println(cache.Allocation()) } // ` fmt.Println("size", kate.Size)` diff --git a/go.mod b/go.mod index c9c161a..74dbe57 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/go-redis/redis/v9 v9.0.0-rc.2 github.com/longbridgeapp/assert v1.1.0 github.com/shamaton/msgpack/v2 v2.1.1 + github.com/ugorji/go/codec v1.2.8 ) require ( diff --git a/go.sum b/go.sum index eaddf9f..e07c0e3 100644 --- a/go.sum +++ b/go.sum @@ -26,6 +26,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ 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= +github.com/ugorji/go/codec v1.2.8 h1:sgBJS6COt0b/P40VouWKdseidkDgHxYGm0SAglUHfP0= +github.com/ugorji/go/codec v1.2.8/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= diff --git a/hypercache.go b/hypercache.go index 72e69f2..0c9853c 100644 --- a/hypercache.go +++ b/hypercache.go @@ -9,6 +9,7 @@ package hypercache import ( "sync" + "sync/atomic" "time" "github.com/hyp3rd/hypercache/backend" @@ -45,6 +46,8 @@ type HyperCache[T backend.IBackendConstrain] struct { 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 + maxCacheSize int64 // maxCacheSize instructs the cache not allocate more memory than this limit, value in MB, 0 means no limit + memoryAllocation atomic.Int64 // memoryAllocation is the current memory allocation of the cache, value in bytes mutex sync.RWMutex // `mutex` holds a RWMutex (Read-Write Mutex) that is used to protect the eviction algorithm from concurrent access 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 @@ -235,7 +238,7 @@ func (hyperCache *HyperCache[T]) expirationLoop() { // iterate all expired items and remove them for _, item := range items { expiredCount++ - hyperCache.backend.Remove(item.Key) + hyperCache.Remove(item.Key) models.ItemPool.Put(item) hyperCache.StatsCollector.Incr("item_expired_count", 1) } @@ -267,7 +270,8 @@ func (hyperCache *HyperCache[T]) evictionLoop() { break } - hyperCache.backend.Remove(key) + // remove the item from the cache + hyperCache.Remove(key) evictedCount++ hyperCache.StatsCollector.Incr("item_evicted_count", 1) } @@ -285,24 +289,10 @@ func (hyperCache *HyperCache[T]) evictItem() (string, bool) { return "", false } - err := hyperCache.backend.Remove(key) - if err != nil { - return "", false - } + hyperCache.Remove(key) return key, true } -// SetCapacity sets the capacity of the cache. If the new capacity is smaller than the current number of items in the cache, -// it evicts the excess items from the cache. -func (hyperCache *HyperCache[T]) SetCapacity(capacity int) { - // set capacity of the backend - hyperCache.backend.SetCapacity(capacity) - // if the cache size is greater than the new capacity, evict items - if hyperCache.backend.Count() > hyperCache.Capacity() { - hyperCache.evictionLoop() - } -} - // Set adds an item to the cache with the given key and value. If an item with the same key already exists, it updates the value of the existing item. // If the expiration duration is greater than zero, the item will expire after the specified duration. // If the capacity of the cache is reached, the cache will leverage the eviction algorithm proactively if the evictionInterval is zero. If not, the background process will take care of the eviction. @@ -317,8 +307,20 @@ func (hyperCache *HyperCache[T]) Set(key string, value any, expiration time.Dura hyperCache.mutex.Lock() defer hyperCache.mutex.Unlock() + // Set the size of the item + err := item.SetSize() + if err != nil { + return err + } + + // check if adding this item will exceed the maxCacheSize + hyperCache.memoryAllocation.Add(item.Size) + if hyperCache.maxCacheSize > 0 && hyperCache.memoryAllocation.Load() > hyperCache.maxCacheSize { + return errors.ErrCacheFull + } + // Insert the item into the cache - err := hyperCache.backend.Set(item) + err = hyperCache.backend.Set(item) if err != nil { models.ItemPool.Put(item) return err @@ -335,43 +337,6 @@ func (hyperCache *HyperCache[T]) Set(key string, value any, expiration time.Dura return nil } -// SetMultiple adds multiple items to the cache with the given key-value pairs. If an item with the same key already exists, it updates the value of the existing item. -func (hyperCache *HyperCache[T]) SetMultiple(items map[string]any, expiration time.Duration) error { - // Create a new cache item and set its properties - cacheItems := make([]*models.Item, 0, len(items)) - for key, value := range items { - item := models.ItemPool.Get().(*models.Item) - item.Key = key - item.Value = value - item.Expiration = expiration - item.LastAccess = time.Now() - cacheItems = append(cacheItems, item) - } - - hyperCache.mutex.Lock() - defer hyperCache.mutex.Unlock() - - // Insert the items into the cache - for _, item := range cacheItems { - err := hyperCache.backend.Set(item) - if err != nil { - for _, item := range cacheItems { - models.ItemPool.Put(item) - } - return err - } - // Set the item in the eviction algorithm - hyperCache.evictionAlgorithm.Set(item.Key, item.Value) - } - - // If the cache is at capacity, evict an item when the eviction interval is zero - if hyperCache.evictionInterval == 0 && hyperCache.backend.Capacity() > 0 && hyperCache.backend.Count() > hyperCache.backend.Capacity() { - hyperCache.evictionLoop() - } - - return nil -} - // Get retrieves the item with the given key from the cache. func (hyperCache *HyperCache[T]) Get(key string) (value any, ok bool) { item, ok := hyperCache.backend.Get(key) @@ -444,7 +409,21 @@ func (hyperCache *HyperCache[T]) GetOrSet(key string, value any, expiration time hyperCache.mutex.Lock() defer hyperCache.mutex.Unlock() - err := hyperCache.backend.Set(item) + + // Set the size of the item + err := item.SetSize() + if err != nil { + return nil, err + } + + // check if adding this item will exceed the maxCacheSize + hyperCache.memoryAllocation.Add(item.Size) + if hyperCache.maxCacheSize > 0 && hyperCache.memoryAllocation.Load() > hyperCache.maxCacheSize { + return nil, errors.ErrCacheFull + } + + // Insert the item into the cache + err = hyperCache.backend.Set(item) if err != nil { models.ItemPool.Put(item) return nil, err @@ -454,7 +433,7 @@ func (hyperCache *HyperCache[T]) GetOrSet(key string, value any, expiration time // Set the item in the eviction algorithm hyperCache.evictionAlgorithm.Set(key, item.Value) // If the cache is at capacity, evict an item when the eviction interval is zero - if hyperCache.evictionInterval == 0 && hyperCache.Capacity() > 0 && hyperCache.Size() > hyperCache.Capacity() { + if hyperCache.evictionInterval == 0 && hyperCache.backend.Capacity() > 0 && hyperCache.backend.Count() > hyperCache.backend.Capacity() { models.ItemPool.Put(item) hyperCache.evictItem() } @@ -549,10 +528,17 @@ func listRedis(cacheBackend *backend.Redis) listFunc { // 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...) + // Remove the item from the eviction algorithm + // and update the memory allocation for _, key := range keys { - hyperCache.evictionAlgorithm.Delete(key) + item, ok := hyperCache.backend.Get(key) + if ok { + // remove the item from the cacheBackend and update the memory allocation + hyperCache.memoryAllocation.Add(-item.Size) + hyperCache.evictionAlgorithm.Delete(key) + } } + hyperCache.backend.Remove(keys...) } // Clear removes all items from the cache. @@ -577,6 +563,9 @@ func (hyperCache *HyperCache[T]) Clear() error { for _, item := range items { hyperCache.evictionAlgorithm.Delete(item.Key) } + + // reset the memory allocation + hyperCache.memoryAllocation.Store(0) return err } @@ -585,8 +574,29 @@ func (hyperCache *HyperCache[T]) Capacity() int { return hyperCache.backend.Capacity() } -// Size returns the number of items in the cache. -func (hyperCache *HyperCache[T]) Size() int { +// SetCapacity sets the capacity of the cache. If the new capacity is smaller than the current number of items in the cache, +// it evicts the excess items from the cache. +func (hyperCache *HyperCache[T]) SetCapacity(capacity int) { + // set capacity of the backend + hyperCache.backend.SetCapacity(capacity) + // if the cache size is greater than the new capacity, evict items + if hyperCache.backend.Count() > hyperCache.Capacity() { + hyperCache.evictionLoop() + } +} + +// Allocation returns the size allocation in bytes of the current cache. +func (hyperCache *HyperCache[T]) Allocation() int64 { + return hyperCache.memoryAllocation.Load() +} + +// MaxCacheSize returns the maximum size in bytes of the cache. +func (hyperCache *HyperCache[T]) MaxCacheSize() int64 { + return hyperCache.maxCacheSize +} + +// Count returns the number of items in the cache. +func (hyperCache *HyperCache[T]) Count() int { return hyperCache.backend.Count() } diff --git a/middleware/logging.go b/middleware/logging.go index 471cefe..6c14274 100644 --- a/middleware/logging.go +++ b/middleware/logging.go @@ -107,14 +107,19 @@ func (mw LoggingMiddleware) Clear() error { return mw.next.Clear() } -// Capacity logs the time it takes to execute the next middleware. +// Capacity takes to execute the next middleware. func (mw LoggingMiddleware) Capacity() int { return mw.next.Capacity() } -// Size logs the time it takes to execute the next middleware. -func (mw LoggingMiddleware) Size() int { - return mw.next.Size() +// Allocation returns the size allocation in bytes cache +func (mw LoggingMiddleware) Allocation() int64 { + return mw.next.Allocation() +} + +// Count takes to execute the next middleware. +func (mw LoggingMiddleware) Count() int { + return mw.next.Count() } // TriggerEviction logs the time it takes to execute the next middleware. diff --git a/middleware/stats.go b/middleware/stats.go index 4d429b6..9a9d692 100644 --- a/middleware/stats.go +++ b/middleware/stats.go @@ -115,9 +115,14 @@ func (mw StatsCollectorMiddleware) TriggerEviction() { mw.next.TriggerEviction() } -// Size returns the size of the cache -func (mw StatsCollectorMiddleware) Size() int { - return mw.next.Size() +// Allocation returns the size allocation in bytes cache +func (mw StatsCollectorMiddleware) Allocation() int64 { + return mw.next.Allocation() +} + +// Countze returns the count of the items in the cache +func (mw StatsCollectorMiddleware) Count() int { + return mw.next.Count() } // Stop collects the stats for Stop methods and stops the cache and all its goroutines (if any) diff --git a/models/item.go b/models/item.go index 623e16d..534f9a5 100644 --- a/models/item.go +++ b/models/item.go @@ -3,15 +3,13 @@ package models // Item represents an item in the cache. It has a key, value, expiration duration, and a last access time field. import ( - "bytes" - "encoding/gob" "strings" "sync" "sync/atomic" "time" "github.com/hyp3rd/hypercache/errors" - // "github.com/ugorji/go/codec" + "github.com/ugorji/go/codec" ) var ( @@ -23,65 +21,64 @@ var ( } // buf is a buffer used to calculate the size of the item. - buf bytes.Buffer + // buf bytes.Buffer + + // encoderPool is a pool of encoders used to calculate the size of the item. + // encoderPool = sync.Pool{ + // New: func() any { + // return gob.NewEncoder(&buf) + // }, + // } + + buf []byte // encoderPool is a pool of encoders used to calculate the size of the item. encoderPool = sync.Pool{ New: func() any { - return gob.NewEncoder(&buf) + return codec.NewEncoderBytes(&buf, &codec.CborHandle{}) }, } - - // b []byte - - // // encoderPool is a pool of encoders used to calculate the size of the item. - // encoderPool2 = sync.Pool{ - // New: func() any { - // return codec.NewEncoderBytes(&b, &codec.CborHandle{}) - // }, - // } ) // Item is a struct that represents an item in the cache. It has a key, value, expiration duration, and a last access time field. type Item struct { Key string // key of the item Value any // Value of the item - Size int // Size of the item, in bytes + Size int64 // Size of the item, in bytes Expiration time.Duration // Expiration duration of the item LastAccess time.Time // LastAccess time of the item AccessCount uint // AccessCount of times the item has been accessed } // Size returns the size of the Item in bytes -func (i *Item) SetSize() error { - // Get an encoder from the pool - enc := encoderPool.Get().(*gob.Encoder) +// func (i *Item) SetSize() error { +// // Get an encoder from the pool +// enc := encoderPool.Get().(*gob.Encoder) +// // Reset the buffer and put the encoder back in the pool +// defer buf.Reset() +// defer encoderPool.Put(enc) + +// // Encode the item +// if err := enc.Encode(i.Value); err != nil { +// return errors.ErrInvalidSize +// } +// // Set the size of the item +// i.Size = int64(buf.Len()) +// return nil +// } - // Encode the item +func (i *Item) SetSize() error { + enc := encoderPool.Get().(*codec.Encoder) + defer encoderPool.Put(enc) if err := enc.Encode(i.Value); err != nil { return errors.ErrInvalidSize } - // Set the size of the item - i.Size = buf.Len() - // Reset the buffer and put the encoder back in the pool - buf.Reset() - encoderPool.Put(enc) + + i.Size = int64(len(buf)) + buf = buf[:0] return nil } -// func (i *Item) SetSizev2() error { -// // var b []byte -// // enc := codec.NewEncoderBytes(&b, &codec.CborHandle{}) -// enc := encoderPool2.Get().(*codec.Encoder) -// if err := enc.Encode(i.Value); err != nil { -// return errors.ErrInvalidSize -// } -// i.Size = len(b) -// b = b[:0] -// encoderPool2.Put(enc) -// return nil -// } - // SizeMB returns the size of the Item in megabytes func (i *Item) SizeMB() float64 { return float64(i.Size) / (1024 * 1024) diff --git a/service.go b/service.go index e915103..2e901a6 100644 --- a/service.go +++ b/service.go @@ -28,8 +28,10 @@ type Service interface { Clear() error // Capacity returns the capacity of the cache Capacity() int - // Size returns the number of items in the cache - Size() int + // Allocation returns the allocation in bytes of the current cache + Allocation() int64 + // Count returns the number of items in the cache + Count() int // TriggerEviction triggers the eviction of the cache TriggerEviction() // Stop stops the cache diff --git a/tests/hypercache_get_or_set_test.go b/tests/hypercache_get_or_set_test.go index f02d40e..37afc58 100644 --- a/tests/hypercache_get_or_set_test.go +++ b/tests/hypercache_get_or_set_test.go @@ -48,7 +48,7 @@ func TestHyperCache_GetOrSet(t *testing.T) { value: nil, expiry: 0, expectedValue: nil, - expectedErr: errors.ErrNilValue, + expectedErr: errors.ErrInvalidSize, }, { name: "get or set with key that has expired", diff --git a/tests/hypercache_set_test.go b/tests/hypercache_set_test.go index f544b48..57d71b0 100644 --- a/tests/hypercache_set_test.go +++ b/tests/hypercache_set_test.go @@ -48,7 +48,7 @@ func TestHyperCache_Set(t *testing.T) { value: nil, expiry: 0, expectedValue: nil, - expectedErr: errors.ErrNilValue, + expectedErr: errors.ErrInvalidSize, }, { name: "overwrite existing key", From d663203ab004d4d9e655a7c3505ebc95ee8b9a15 Mon Sep 17 00:00:00 2001 From: Francesco Cosentino Date: Thu, 26 Jan 2023 00:29:50 +0100 Subject: [PATCH 8/9] added size handling, improved redis, updated modules --- README.md | 13 +-- backend/inmemory.go | 1 - backend/inmemory.go.bak | 168 --------------------------------------- backend/options.go | 2 +- backend/redis.go | 12 +-- backend/redis/options.go | 2 +- backend/redis/store.go | 2 +- examples/redis/redis.go | 8 +- go.mod | 5 +- go.sum | 17 ++-- 10 files changed, 26 insertions(+), 204 deletions(-) delete mode 100644 backend/inmemory.go.bak diff --git a/README.md b/README.md index 52d1d37..25916f4 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## Synopsis -HyperCache is a **thread-safe** **high-performance** cache implementation in Go that supports multiple backends with the expiration and eviction of items supporting custom algorithms alongside the defaults. It can be used as a standalone cache or as a cache middleware for a service. It can implement a [service interface](./service.go) to intercept cache methods and decorate em with middleware (default or custom). +HyperCache is a **thread-safe** **high-performance** cache implementation in `Go` that supports multiple backends with optional size limit, expiration and eviction of items supporting custom algorithms alongside the defaults. It can be used as a standalone cache or as a cache middleware for a service. It can implement a [service interface](./service.go) to intercept and decorate the cache methods with middleware (default or custom). It is optimized for performance and flexibility allowing to specify the expiration and eviction intervals, provide and register new eviction algorithms, stats collectors, middleware(s). It ships with a default [historigram stats collector](./stats/statscollector.go) and several eviction algorithms, but you can develop and register your own as long as it implements the [Eviction Algorithm interface](./eviction/eviction.go).: @@ -50,12 +50,13 @@ goos: darwin goarch: amd64 pkg: github.com/hyp3rd/hypercache/tests/benchmark cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz -BenchmarkHyperCache_Get-16 38833602 123.9 ns/op 0 B/op 0 allocs/op -BenchmarkHyperCache_Get_ProactiveEviction-16 38079158 124.4 ns/op 0 B/op 0 allocs/op -BenchmarkHyperCache_Set-16 4361000 1217 ns/op 203 B/op 3 allocs/op -BenchmarkHyperCache_Set_Proactive_Eviction-16 4343996 1128 ns/op 92 B/op 3 allocs/op +BenchmarkHyperCache_Get-16 39429110 115.7 ns/op 0 B/op 0 allocs/op +BenchmarkHyperCache_Get_ProactiveEviction-16 42094736 118.0 ns/op 0 B/op 0 allocs/op +BenchmarkHyperCache_List-16 10898176 437.0 ns/op 85 B/op 1 allocs/op +BenchmarkHyperCache_Set-16 3034786 1546 ns/op 252 B/op 4 allocs/op +BenchmarkHyperCache_Set_Proactive_Eviction-16 2725557 1833 ns/op 162 B/op 3 allocs/op PASS -ok github.com/hyp3rd/hypercache/tests/benchmark 23.723s +ok github.com/hyp3rd/hypercache/tests/benchmark 30.031s ``` ### Examples diff --git a/backend/inmemory.go b/backend/inmemory.go index 03626e4..a75af02 100644 --- a/backend/inmemory.go +++ b/backend/inmemory.go @@ -23,7 +23,6 @@ type InMemory struct { // NewInMemory creates a new in-memory cache with the given options. func NewInMemory[T InMemory](opts ...Option[InMemory]) (backend IInMemory[T], err error) { InMemory := &InMemory{ - // items: datastructure.New[*models.Item](), items: datastructure.New(), } // Apply the backend options diff --git a/backend/inmemory.go.bak b/backend/inmemory.go.bak deleted file mode 100644 index 52c42c6..0000000 --- a/backend/inmemory.go.bak +++ /dev/null @@ -1,168 +0,0 @@ -package backend - -import ( - "fmt" - "sort" - "sync" - - "github.com/hyp3rd/hypercache/datastructure" - "github.com/hyp3rd/hypercache/errors" - "github.com/hyp3rd/hypercache/models" - "github.com/hyp3rd/hypercache/types" -) - -// InMemory is a cache backend that stores the items in memory, leveraging a custom `ConcurrentMap`. -type InMemory struct { - items datastructure.ConcurrentMap[string, *models.Item] // map to store the items in the cache - capacity int // capacity of the cache, limits the number of items that can be stored in the cache - maxCacheSize int // maxCacheSize instructs the cache not allocate more memory than this limit, value in MB, 0 means no limit - memoryAllocation int // memoryAllocation is the current memory allocation of the cache, value in bytes - mutex sync.RWMutex // mutex to protect the cache from concurrent access - SortFilters // filters applied when listing the items in the cache -} - -// NewInMemory creates a new in-memory cache with the given options. -func NewInMemory[T InMemory](opts ...Option[InMemory]) (backend IInMemory[T], err error) { - InMemory := &InMemory{ - items: datastructure.New[*models.Item](), - } - // Apply the backend options - ApplyOptions(InMemory, opts...) - // Check if the `capacity` is valid - if InMemory.capacity < 0 { - return nil, errors.ErrInvalidCapacity - } - // Check if the `maxCacheSize` is valid - if InMemory.maxCacheSize < 0 { - return nil, errors.ErrInvalidMaxCacheSize - } - - return InMemory, nil -} - -// SetCapacity sets the capacity of the cache. -func (cacheBackend *InMemory) SetCapacity(capacity int) { - if capacity < 0 { - return - } - cacheBackend.capacity = capacity -} - -// Capacity returns the capacity of the cacheBackend. -func (cacheBackend *InMemory) Capacity() int { - return cacheBackend.capacity -} - -// Count returns the number of items in the cache. -func (cacheBackend *InMemory) Count() int { - return cacheBackend.items.Count() -} - -// Size returns the number of items in the cacheBackend. -func (cacheBackend *InMemory) Size() int { - return cacheBackend.memoryAllocation -} - -// MaxCacheSize returns the maximum size in bytes of the cacheBackend. -func (cacheBackend *InMemory) MaxCacheSize() int { - return cacheBackend.maxCacheSize -} - -// Get retrieves the item with the given key from the cacheBackend. If the item is not found, it returns nil. -func (cacheBackend *InMemory) Get(key string) (item *models.Item, ok bool) { - item, ok = cacheBackend.items.Get(key) - if !ok { - return nil, false - } - // return the item - return item, true -} - -// Set adds a Item to the cache. -func (cacheBackend *InMemory) Set(item *models.Item) error { - // Check for invalid key, value, or duration - if err := item.Valid(); err != nil { - models.ItemPool.Put(item) - return err - } - - cacheBackend.mutex.Lock() - defer cacheBackend.mutex.Unlock() - // Set the size of the item - err := item.SetSize() - if err != nil { - return err - } - - // check if adding this item will exceed the maxCacheSize - cacheBackend.memoryAllocation = cacheBackend.memoryAllocation + item.Size - if cacheBackend.maxCacheSize > 0 && cacheBackend.memoryAllocation > cacheBackend.maxCacheSize { - return errors.ErrCacheFull - } - - cacheBackend.items.Set(item.Key, item) - return nil -} - -// List returns a list of all items in the cache filtered and ordered by the given options -func (cacheBackend *InMemory) List(options ...FilterOption[InMemory]) ([]*models.Item, error) { - // Apply the filter options - ApplyFilterOptions(cacheBackend, options...) - - items := make([]*models.Item, 0, cacheBackend.items.Count()) - for item := range cacheBackend.items.IterBuffered() { - if cacheBackend.FilterFunc == nil || cacheBackend.FilterFunc(item.Val) { - items = append(items, item.Val) - } - } - - if cacheBackend.SortBy == "" { - return items, nil - } - - var sorter sort.Interface - switch cacheBackend.SortBy { - case types.SortByKey.String(): - sorter = &itemSorterByKey{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 -} - -// Remove removes items with the given key from the cacheBackend. If an item is not found, it does nothing. -func (cacheBackend *InMemory) Remove(keys ...string) (err error) { - //TODO: determine if handling the error or not - var ok bool - item := models.ItemPool.Get().(*models.Item) - defer models.ItemPool.Put(item) - for _, key := range keys { - item, ok = cacheBackend.items.Get(key) - if ok { - // remove the item from the cacheBackend and update the memory allocation - cacheBackend.memoryAllocation = cacheBackend.memoryAllocation - item.Size - cacheBackend.items.Remove(key) - } - } - return -} - -// Clear removes all items from the cacheBackend. -func (cacheBackend *InMemory) Clear() { - // clear the cacheBackend - cacheBackend.items.Clear() - // reset the memory allocation - cacheBackend.memoryAllocation = 0 -} diff --git a/backend/options.go b/backend/options.go index b0ab041..575dc22 100644 --- a/backend/options.go +++ b/backend/options.go @@ -1,10 +1,10 @@ package backend import ( - "github.com/go-redis/redis/v9" "github.com/hyp3rd/hypercache/libs/serializer" "github.com/hyp3rd/hypercache/models" "github.com/hyp3rd/hypercache/types" + "github.com/redis/go-redis/v9" ) // ISortableBackend is an interface that defines the methods that a backend should implement to be sortable. diff --git a/backend/redis.go b/backend/redis.go index 778e02d..30fbc6c 100644 --- a/backend/redis.go +++ b/backend/redis.go @@ -5,11 +5,11 @@ import ( "fmt" "sort" - "github.com/go-redis/redis/v9" "github.com/hyp3rd/hypercache/errors" "github.com/hyp3rd/hypercache/libs/serializer" "github.com/hyp3rd/hypercache/models" "github.com/hyp3rd/hypercache/types" + "github.com/redis/go-redis/v9" ) // Redis is a cache backend that stores the items in a redis implementation. @@ -75,10 +75,8 @@ func (cacheBackend *Redis) Count() int { // Get retrieves the Item with the given key from the cacheBackend. If the item is not found, it returns nil. 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() if err != nil { return nil, false } @@ -92,8 +90,6 @@ func (cacheBackend *Redis) Get(key string) (item *models.Item, ok bool) { 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 { // Check if the item is not found if err == redis.Nil { @@ -119,12 +115,6 @@ func (cacheBackend *Redis) Set(item *models.Item) error { return err } - // Set the size of the item - err := item.SetSize() - if err != nil { - return err - } - // Serialize the item data, err := cacheBackend.Serializer.Marshal(item) if err != nil { diff --git a/backend/redis/options.go b/backend/redis/options.go index 21e28aa..0ca01aa 100644 --- a/backend/redis/options.go +++ b/backend/redis/options.go @@ -4,7 +4,7 @@ import ( "crypto/tls" "time" - "github.com/go-redis/redis/v9" + "github.com/redis/go-redis/v9" ) // Option is a function type that can be used to configure the `Redis`. diff --git a/backend/redis/store.go b/backend/redis/store.go index 06ed585..8f5e0a4 100644 --- a/backend/redis/store.go +++ b/backend/redis/store.go @@ -7,7 +7,7 @@ import ( "strings" "time" - "github.com/go-redis/redis/v9" + "github.com/redis/go-redis/v9" ) // Store is a redis store instance with redis client diff --git a/examples/redis/redis.go b/examples/redis/redis.go index f4bfc02..1c7705f 100644 --- a/examples/redis/redis.go +++ b/examples/redis/redis.go @@ -29,7 +29,7 @@ func main() { }, HyperCacheOptions: []hypercache.Option[backend.Redis]{ hypercache.WithEvictionInterval[backend.Redis](time.Second * 5), - hypercache.WithEvictionAlgorithm[backend.Redis]("lru"), + hypercache.WithEvictionAlgorithm[backend.Redis]("clock"), }, } @@ -69,7 +69,7 @@ func main() { fmt.Println("count", hyperCache.Count()) fmt.Println("capacity", hyperCache.Capacity()) - time.Sleep(time.Second * 10) + time.Sleep(time.Second * 5) allItems, err = hyperCache.List( backend.WithSortBy[backend.Redis](types.SortByKey), @@ -88,7 +88,9 @@ func main() { fmt.Println(item.Key, item.Value) } - value, ok := hyperCache.Get("key-9") + fmt.Println("count", hyperCache.Count()) + + value, ok := hyperCache.Get("key-49") if ok { fmt.Println(value) } diff --git a/go.mod b/go.mod index 74dbe57..7c1d61e 100644 --- a/go.mod +++ b/go.mod @@ -3,17 +3,16 @@ module github.com/hyp3rd/hypercache go 1.19 require ( - github.com/go-redis/redis/v9 v9.0.0-rc.2 github.com/longbridgeapp/assert v1.1.0 + github.com/redis/go-redis/v9 v9.0.0-rc.4 github.com/shamaton/msgpack/v2 v2.1.1 github.com/ugorji/go/codec v1.2.8 ) require ( - github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // 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.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index e07c0e3..e6de9f0 100644 --- a/go.sum +++ b/go.sum @@ -1,22 +1,21 @@ -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/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/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/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/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/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= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/gomega v1.24.2 h1:J/tulyYK6JwBldPViHJReihxxZ+22FHs0piGjQAvoUE= -github.com/onsi/gomega v1.24.2/go.mod h1:gs3J10IS7Z7r7eXRoNJIrNqU4ToQukCJhFtKrWgHWnk= +github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.0.0-rc.4 h1:JUhsiZMTZknz3vn50zSVlkwcSeTGPd51lMO3IKUrWpY= +github.com/redis/go-redis/v9 v9.0.0-rc.4/go.mod h1:Vo3EsyWnicKnSKCA7HhgnvnyA74wOA69Cd2Meli5mmA= github.com/shamaton/msgpack/v2 v2.1.1 h1:gAMxOtVJz93R0EwewwUc8tx30n34aV6BzJuwHE8ogAk= 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= @@ -28,9 +27,9 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/ugorji/go/codec v1.2.8 h1:sgBJS6COt0b/P40VouWKdseidkDgHxYGm0SAglUHfP0= github.com/ugorji/go/codec v1.2.8/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= -golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= -golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= +golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= 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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= From f2d2cf059f24cfc31b0abf7d4875c8d85489ee14 Mon Sep 17 00:00:00 2001 From: Francesco Cosentino Date: Thu, 26 Jan 2023 00:35:14 +0100 Subject: [PATCH 9/9] fixed tests --- tests/hypercache_get_or_set_test.go | 2 +- tests/hypercache_set_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/hypercache_get_or_set_test.go b/tests/hypercache_get_or_set_test.go index 37afc58..f02d40e 100644 --- a/tests/hypercache_get_or_set_test.go +++ b/tests/hypercache_get_or_set_test.go @@ -48,7 +48,7 @@ func TestHyperCache_GetOrSet(t *testing.T) { value: nil, expiry: 0, expectedValue: nil, - expectedErr: errors.ErrInvalidSize, + expectedErr: errors.ErrNilValue, }, { name: "get or set with key that has expired", diff --git a/tests/hypercache_set_test.go b/tests/hypercache_set_test.go index 57d71b0..f544b48 100644 --- a/tests/hypercache_set_test.go +++ b/tests/hypercache_set_test.go @@ -48,7 +48,7 @@ func TestHyperCache_Set(t *testing.T) { value: nil, expiry: 0, expectedValue: nil, - expectedErr: errors.ErrInvalidSize, + expectedErr: errors.ErrNilValue, }, { name: "overwrite existing key",