From 835a80b5bf9323175daebff6db351687282be388 Mon Sep 17 00:00:00 2001 From: Philip Gribov Date: Fri, 22 Nov 2024 23:48:45 +0300 Subject: [PATCH 1/2] Add size cache --- .gitignore | 1 + db/db.go | 2 +- go.mod | 2 +- lib/cache/cache.go | 233 +++++++++++++++++++++++++++++++++++++++++++ lib/cache/errors.go | 27 +++++ lib/logger/logger.go | 5 + 6 files changed, 268 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 lib/cache/cache.go create mode 100644 lib/cache/errors.go create mode 100644 lib/logger/logger.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/db/db.go b/db/db.go index 5f792cc..3ca873e 100644 --- a/db/db.go +++ b/db/db.go @@ -4,7 +4,7 @@ import ( "gorm.io/driver/postgres" "gorm.io/gorm" "log/slog" - "main/db/models" + "testing_system/db/models" ) func NewDb(config Config) (*gorm.DB, error) { diff --git a/go.mod b/go.mod index 71d010b..e6decde 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module main +module testing_system go 1.22 diff --git a/lib/cache/cache.go b/lib/cache/cache.go new file mode 100644 index 0000000..266d9aa --- /dev/null +++ b/lib/cache/cache.go @@ -0,0 +1,233 @@ +package cache + +import ( + "container/list" + "sync" + "testing_system/lib/logger" +) + +type valHolder[TValue any] struct { + Value *TValue + Error error + + LockCount uint64 + Size uint64 + LoadingStatus *sync.WaitGroup + ListPosition *list.Element +} + +// LRUSizeCache is simple key value LRU cache that accepts size bound for values +// Cache has Getter and may have Remover functions +// Cache removes the least recently used value if the total size bound is exceeded +// +// Getter will be called to get value for specified key +// If value can be loaded, getter must return value, and size of value +// If value can not be loaded, getter must return error and size of value (even with error) +// +// # If Remover is specified, and item has been loaded without error, Remover will be called before value removal +// +// Lock and Unlock methods can be used to avoid removal of specific keys +type LRUSizeCache[TKey comparable, TValue any] struct { + mutex sync.Mutex + valueHolders map[TKey]*valHolder[TValue] + + getter func(TKey) (*TValue, error, uint64) + remover func(TKey, *TValue) + + sizeBound uint64 + totalSize uint64 + + recentRank *list.List +} + +// NewLRUSizeCache creates new cache for given size bound +func NewLRUSizeCache[TKey comparable, TValue any]( + sizeBound uint64, + getter func(TKey) (*TValue, error, uint64), + remover func(TKey, *TValue), +) *LRUSizeCache[TKey, TValue] { + return &LRUSizeCache[TKey, TValue]{ + valueHolders: make(map[TKey]*valHolder[TValue]), + + getter: getter, + remover: remover, + + sizeBound: sizeBound, + totalSize: 0, + + recentRank: list.New(), + } +} + +// Get returns item from cache. +// +// If id is not found, it will be loaded and get method will wait until value is loaded. +// If loader returned error, this error will be returned along with item +func (c *LRUSizeCache[TKey, TValue]) Get(key TKey) (*TValue, error) { + c.mutex.Lock() + defer c.mutex.Unlock() + + valueHolder := c.lockStartLoadGetHolder(key) + if valueHolder.LoadingStatus == nil { + valueHolder.LockCount-- + c.itemUsed(key, valueHolder) + return valueHolder.Value, valueHolder.Error + } + + loadingStatus := valueHolder.LoadingStatus + c.mutex.Unlock() + + loadingStatus.Wait() + c.mutex.Lock() + + // Lock count is increased, so value can not be removed + valueHolder = c.valueHolders[key] + valueHolder.LockCount-- + c.itemUsed(key, valueHolder) + // Mutex is deferred at the beginning + return valueHolder.Value, valueHolder.Error +} + +// Lock locks item in cache to avoid its removal. +// +// If element is not present in cache, it will start loading in background +// Multiple lock can be added to item, they will require multiple unlock calls +func (c *LRUSizeCache[TKey, TValue]) Lock(key TKey) { + c.mutex.Lock() + defer c.mutex.Unlock() + valueHolder := c.lockStartLoadGetHolder(key) + if valueHolder.LoadingStatus == nil { + c.itemUsed(key, valueHolder) // Only loaded items can appear in RankList + } +} + +// Unlock unlocks item in cache so that it can be removed. +// +// If item is not found, ErrItemNotFound will be returned +// If item is not locked, ErrNotLocked will be returned +func (c *LRUSizeCache[TKey, TValue]) Unlock(key TKey) error { + c.mutex.Lock() + defer c.mutex.Unlock() + + valueHolder, ok := c.valueHolders[key] + if ok { + if valueHolder.LockCount > 0 { + valueHolder.LockCount-- + } else { + return &ErrItemNotLocked{key: key} + } + c.removeItemsIfNeeded() + return nil + } else { + return &ErrItemNotFound{key: key} + } +} + +// Remove removes item from cache. +// +// If item does not exist in cache, returns nil. +// If item is locked, returns ErrItemLocked. +func (c *LRUSizeCache[TKey, TValue]) Remove(key TKey) error { + c.mutex.Lock() + defer c.mutex.Unlock() + + valueHolder, ok := c.valueHolders[key] + if !ok { + return nil + } + + if valueHolder.LockCount > 0 { + return &ErrItemLocked{key: key} + } + + c.removeSingleItem(key) + return nil +} + +// Mutex must be locked +// Increases lock count +// If value is loaded, returns valueHolder +// If value is loading, returns valueHolder with active waitgroup +// If value is absent, starts loading in background and returns valueHolder with active waitgroup +func (c *LRUSizeCache[TKey, TValue]) lockStartLoadGetHolder(key TKey) *valHolder[TValue] { + valueHolder, ok := c.valueHolders[key] + if ok { + valueHolder.LockCount++ + return valueHolder + } + valueHolder = &valHolder[TValue]{ + LoadingStatus: &sync.WaitGroup{}, + LockCount: 1, + } + + valueHolder.LoadingStatus.Add(1) + c.valueHolders[key] = valueHolder + + go c.loadAbsentValue(key) + + return valueHolder +} + +// Mutex must not be locked or this should be called in different goroutine +// Value must be absent and no concurrent load for same key must be present +func (c *LRUSizeCache[TKey, TValue]) loadAbsentValue(key TKey) { + value, err, size := c.getter(key) + + c.mutex.Lock() + defer c.mutex.Unlock() + valueHolder := c.valueHolders[key] + + if valueHolder.Value != nil || valueHolder.Error != nil { + logger.Panic("Error in LRUSizeCache. loadAbsentValue is called for already loaded key, key: %v", key) + } + + valueHolder.Value = value + valueHolder.Error = err + + c.totalSize += size + valueHolder.Size = size + valueHolder.LoadingStatus.Done() + valueHolder.LoadingStatus = nil // It will stay in other pointers + + c.itemUsed(key, valueHolder) + c.removeItemsIfNeeded() +} + +// Mutex must be locked +func (c *LRUSizeCache[TKey, TValue]) itemUsed(key TKey, valueHolder *valHolder[TValue]) { + if valueHolder.ListPosition != nil { + c.recentRank.MoveToBack(valueHolder.ListPosition) + } else { + valueHolder.ListPosition = c.recentRank.PushBack(key) + } +} + +// Mutex must be locked +func (c *LRUSizeCache[TKey, TValue]) removeItemsIfNeeded() { + elem := c.recentRank.Front() + for c.totalSize > c.sizeBound && elem != nil { + key := elem.Value.(TKey) + valueHolder := c.valueHolders[key] + elem = elem.Next() + + if valueHolder.LockCount == 0 { + c.removeSingleItem(key) + } + } +} + +// Mutex must be locked +// Key, Value must be present and lock count should be zero +func (c *LRUSizeCache[TKey, TValue]) removeSingleItem(key TKey) { + valueHolder := c.valueHolders[key] + if valueHolder.LockCount != 0 { + logger.Panic("Error in LRUSizeCache. Removing key with non zero lock count, key: %#v", key) + } + if c.remover != nil && valueHolder.Error != nil { + c.remover(key, valueHolder.Value) + } + + delete(c.valueHolders, key) + c.totalSize -= valueHolder.Size + c.recentRank.Remove(valueHolder.ListPosition) +} diff --git a/lib/cache/errors.go b/lib/cache/errors.go new file mode 100644 index 0000000..08b7fc7 --- /dev/null +++ b/lib/cache/errors.go @@ -0,0 +1,27 @@ +package cache + +import "fmt" + +type ErrItemLocked struct { + key interface{} +} + +func (e *ErrItemLocked) Error() string { + return fmt.Sprintf("size_cache: item is locked, key: %#v", e.key) +} + +type ErrItemNotFound struct { + key interface{} +} + +func (e *ErrItemNotFound) Error() string { + return fmt.Sprintf("size_cache: item not found, key: %#v", e.key) +} + +type ErrItemNotLocked struct { + key interface{} +} + +func (e *ErrItemNotLocked) Error() string { + return fmt.Sprintf("size_cache: item locked, key: %#v", e.key) +} diff --git a/lib/logger/logger.go b/lib/logger/logger.go new file mode 100644 index 0000000..2743d14 --- /dev/null +++ b/lib/logger/logger.go @@ -0,0 +1,5 @@ +package logger + +func Panic(format string, v ...interface{}) { + // TODO: create logger +} From c072cef3c7507ec1b141f751974b09662eab3ff1 Mon Sep 17 00:00:00 2001 From: Philip Gribov Date: Sat, 23 Nov 2024 21:02:28 +0300 Subject: [PATCH 2/2] Fixes --- lib/cache/cache.go | 6 +++--- lib/cache/errors.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/cache/cache.go b/lib/cache/cache.go index 266d9aa..cfd35fa 100644 --- a/lib/cache/cache.go +++ b/lib/cache/cache.go @@ -67,7 +67,7 @@ func (c *LRUSizeCache[TKey, TValue]) Get(key TKey) (*TValue, error) { c.mutex.Lock() defer c.mutex.Unlock() - valueHolder := c.lockStartLoadGetHolder(key) + valueHolder := c.lockAndGetHolder(key) if valueHolder.LoadingStatus == nil { valueHolder.LockCount-- c.itemUsed(key, valueHolder) @@ -95,7 +95,7 @@ func (c *LRUSizeCache[TKey, TValue]) Get(key TKey) (*TValue, error) { func (c *LRUSizeCache[TKey, TValue]) Lock(key TKey) { c.mutex.Lock() defer c.mutex.Unlock() - valueHolder := c.lockStartLoadGetHolder(key) + valueHolder := c.lockAndGetHolder(key) if valueHolder.LoadingStatus == nil { c.itemUsed(key, valueHolder) // Only loaded items can appear in RankList } @@ -149,7 +149,7 @@ func (c *LRUSizeCache[TKey, TValue]) Remove(key TKey) error { // If value is loaded, returns valueHolder // If value is loading, returns valueHolder with active waitgroup // If value is absent, starts loading in background and returns valueHolder with active waitgroup -func (c *LRUSizeCache[TKey, TValue]) lockStartLoadGetHolder(key TKey) *valHolder[TValue] { +func (c *LRUSizeCache[TKey, TValue]) lockAndGetHolder(key TKey) *valHolder[TValue] { valueHolder, ok := c.valueHolders[key] if ok { valueHolder.LockCount++ diff --git a/lib/cache/errors.go b/lib/cache/errors.go index 08b7fc7..09b4b4e 100644 --- a/lib/cache/errors.go +++ b/lib/cache/errors.go @@ -23,5 +23,5 @@ type ErrItemNotLocked struct { } func (e *ErrItemNotLocked) Error() string { - return fmt.Sprintf("size_cache: item locked, key: %#v", e.key) + return fmt.Sprintf("size_cache: item not locked, key: %#v", e.key) }