Skip to content

Commit 31b97cd

Browse files
authored
Merge pull request #1 from MichailKon/lib
Add size cache
2 parents 832b3ff + c072cef commit 31b97cd

File tree

6 files changed

+268
-2
lines changed

6 files changed

+268
-2
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.idea

db/db.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import (
44
"gorm.io/driver/postgres"
55
"gorm.io/gorm"
66
"log/slog"
7-
"main/db/models"
7+
"testing_system/db/models"
88
)
99

1010
func NewDb(config Config) (*gorm.DB, error) {

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
module main
1+
module testing_system
22

33
go 1.22
44

lib/cache/cache.go

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
package cache
2+
3+
import (
4+
"container/list"
5+
"sync"
6+
"testing_system/lib/logger"
7+
)
8+
9+
type valHolder[TValue any] struct {
10+
Value *TValue
11+
Error error
12+
13+
LockCount uint64
14+
Size uint64
15+
LoadingStatus *sync.WaitGroup
16+
ListPosition *list.Element
17+
}
18+
19+
// LRUSizeCache is simple key value LRU cache that accepts size bound for values
20+
// Cache has Getter and may have Remover functions
21+
// Cache removes the least recently used value if the total size bound is exceeded
22+
//
23+
// Getter will be called to get value for specified key
24+
// If value can be loaded, getter must return value, and size of value
25+
// If value can not be loaded, getter must return error and size of value (even with error)
26+
//
27+
// # If Remover is specified, and item has been loaded without error, Remover will be called before value removal
28+
//
29+
// Lock and Unlock methods can be used to avoid removal of specific keys
30+
type LRUSizeCache[TKey comparable, TValue any] struct {
31+
mutex sync.Mutex
32+
valueHolders map[TKey]*valHolder[TValue]
33+
34+
getter func(TKey) (*TValue, error, uint64)
35+
remover func(TKey, *TValue)
36+
37+
sizeBound uint64
38+
totalSize uint64
39+
40+
recentRank *list.List
41+
}
42+
43+
// NewLRUSizeCache creates new cache for given size bound
44+
func NewLRUSizeCache[TKey comparable, TValue any](
45+
sizeBound uint64,
46+
getter func(TKey) (*TValue, error, uint64),
47+
remover func(TKey, *TValue),
48+
) *LRUSizeCache[TKey, TValue] {
49+
return &LRUSizeCache[TKey, TValue]{
50+
valueHolders: make(map[TKey]*valHolder[TValue]),
51+
52+
getter: getter,
53+
remover: remover,
54+
55+
sizeBound: sizeBound,
56+
totalSize: 0,
57+
58+
recentRank: list.New(),
59+
}
60+
}
61+
62+
// Get returns item from cache.
63+
//
64+
// If id is not found, it will be loaded and get method will wait until value is loaded.
65+
// If loader returned error, this error will be returned along with item
66+
func (c *LRUSizeCache[TKey, TValue]) Get(key TKey) (*TValue, error) {
67+
c.mutex.Lock()
68+
defer c.mutex.Unlock()
69+
70+
valueHolder := c.lockAndGetHolder(key)
71+
if valueHolder.LoadingStatus == nil {
72+
valueHolder.LockCount--
73+
c.itemUsed(key, valueHolder)
74+
return valueHolder.Value, valueHolder.Error
75+
}
76+
77+
loadingStatus := valueHolder.LoadingStatus
78+
c.mutex.Unlock()
79+
80+
loadingStatus.Wait()
81+
c.mutex.Lock()
82+
83+
// Lock count is increased, so value can not be removed
84+
valueHolder = c.valueHolders[key]
85+
valueHolder.LockCount--
86+
c.itemUsed(key, valueHolder)
87+
// Mutex is deferred at the beginning
88+
return valueHolder.Value, valueHolder.Error
89+
}
90+
91+
// Lock locks item in cache to avoid its removal.
92+
//
93+
// If element is not present in cache, it will start loading in background
94+
// Multiple lock can be added to item, they will require multiple unlock calls
95+
func (c *LRUSizeCache[TKey, TValue]) Lock(key TKey) {
96+
c.mutex.Lock()
97+
defer c.mutex.Unlock()
98+
valueHolder := c.lockAndGetHolder(key)
99+
if valueHolder.LoadingStatus == nil {
100+
c.itemUsed(key, valueHolder) // Only loaded items can appear in RankList
101+
}
102+
}
103+
104+
// Unlock unlocks item in cache so that it can be removed.
105+
//
106+
// If item is not found, ErrItemNotFound will be returned
107+
// If item is not locked, ErrNotLocked will be returned
108+
func (c *LRUSizeCache[TKey, TValue]) Unlock(key TKey) error {
109+
c.mutex.Lock()
110+
defer c.mutex.Unlock()
111+
112+
valueHolder, ok := c.valueHolders[key]
113+
if ok {
114+
if valueHolder.LockCount > 0 {
115+
valueHolder.LockCount--
116+
} else {
117+
return &ErrItemNotLocked{key: key}
118+
}
119+
c.removeItemsIfNeeded()
120+
return nil
121+
} else {
122+
return &ErrItemNotFound{key: key}
123+
}
124+
}
125+
126+
// Remove removes item from cache.
127+
//
128+
// If item does not exist in cache, returns nil.
129+
// If item is locked, returns ErrItemLocked.
130+
func (c *LRUSizeCache[TKey, TValue]) Remove(key TKey) error {
131+
c.mutex.Lock()
132+
defer c.mutex.Unlock()
133+
134+
valueHolder, ok := c.valueHolders[key]
135+
if !ok {
136+
return nil
137+
}
138+
139+
if valueHolder.LockCount > 0 {
140+
return &ErrItemLocked{key: key}
141+
}
142+
143+
c.removeSingleItem(key)
144+
return nil
145+
}
146+
147+
// Mutex must be locked
148+
// Increases lock count
149+
// If value is loaded, returns valueHolder
150+
// If value is loading, returns valueHolder with active waitgroup
151+
// If value is absent, starts loading in background and returns valueHolder with active waitgroup
152+
func (c *LRUSizeCache[TKey, TValue]) lockAndGetHolder(key TKey) *valHolder[TValue] {
153+
valueHolder, ok := c.valueHolders[key]
154+
if ok {
155+
valueHolder.LockCount++
156+
return valueHolder
157+
}
158+
valueHolder = &valHolder[TValue]{
159+
LoadingStatus: &sync.WaitGroup{},
160+
LockCount: 1,
161+
}
162+
163+
valueHolder.LoadingStatus.Add(1)
164+
c.valueHolders[key] = valueHolder
165+
166+
go c.loadAbsentValue(key)
167+
168+
return valueHolder
169+
}
170+
171+
// Mutex must not be locked or this should be called in different goroutine
172+
// Value must be absent and no concurrent load for same key must be present
173+
func (c *LRUSizeCache[TKey, TValue]) loadAbsentValue(key TKey) {
174+
value, err, size := c.getter(key)
175+
176+
c.mutex.Lock()
177+
defer c.mutex.Unlock()
178+
valueHolder := c.valueHolders[key]
179+
180+
if valueHolder.Value != nil || valueHolder.Error != nil {
181+
logger.Panic("Error in LRUSizeCache. loadAbsentValue is called for already loaded key, key: %v", key)
182+
}
183+
184+
valueHolder.Value = value
185+
valueHolder.Error = err
186+
187+
c.totalSize += size
188+
valueHolder.Size = size
189+
valueHolder.LoadingStatus.Done()
190+
valueHolder.LoadingStatus = nil // It will stay in other pointers
191+
192+
c.itemUsed(key, valueHolder)
193+
c.removeItemsIfNeeded()
194+
}
195+
196+
// Mutex must be locked
197+
func (c *LRUSizeCache[TKey, TValue]) itemUsed(key TKey, valueHolder *valHolder[TValue]) {
198+
if valueHolder.ListPosition != nil {
199+
c.recentRank.MoveToBack(valueHolder.ListPosition)
200+
} else {
201+
valueHolder.ListPosition = c.recentRank.PushBack(key)
202+
}
203+
}
204+
205+
// Mutex must be locked
206+
func (c *LRUSizeCache[TKey, TValue]) removeItemsIfNeeded() {
207+
elem := c.recentRank.Front()
208+
for c.totalSize > c.sizeBound && elem != nil {
209+
key := elem.Value.(TKey)
210+
valueHolder := c.valueHolders[key]
211+
elem = elem.Next()
212+
213+
if valueHolder.LockCount == 0 {
214+
c.removeSingleItem(key)
215+
}
216+
}
217+
}
218+
219+
// Mutex must be locked
220+
// Key, Value must be present and lock count should be zero
221+
func (c *LRUSizeCache[TKey, TValue]) removeSingleItem(key TKey) {
222+
valueHolder := c.valueHolders[key]
223+
if valueHolder.LockCount != 0 {
224+
logger.Panic("Error in LRUSizeCache. Removing key with non zero lock count, key: %#v", key)
225+
}
226+
if c.remover != nil && valueHolder.Error != nil {
227+
c.remover(key, valueHolder.Value)
228+
}
229+
230+
delete(c.valueHolders, key)
231+
c.totalSize -= valueHolder.Size
232+
c.recentRank.Remove(valueHolder.ListPosition)
233+
}

lib/cache/errors.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package cache
2+
3+
import "fmt"
4+
5+
type ErrItemLocked struct {
6+
key interface{}
7+
}
8+
9+
func (e *ErrItemLocked) Error() string {
10+
return fmt.Sprintf("size_cache: item is locked, key: %#v", e.key)
11+
}
12+
13+
type ErrItemNotFound struct {
14+
key interface{}
15+
}
16+
17+
func (e *ErrItemNotFound) Error() string {
18+
return fmt.Sprintf("size_cache: item not found, key: %#v", e.key)
19+
}
20+
21+
type ErrItemNotLocked struct {
22+
key interface{}
23+
}
24+
25+
func (e *ErrItemNotLocked) Error() string {
26+
return fmt.Sprintf("size_cache: item not locked, key: %#v", e.key)
27+
}

lib/logger/logger.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package logger
2+
3+
func Panic(format string, v ...interface{}) {
4+
// TODO: create logger
5+
}

0 commit comments

Comments
 (0)