From 23318446f6a60840ab2c9058f41c895d201f8856 Mon Sep 17 00:00:00 2001 From: Adam Briles Date: Mon, 15 Aug 2022 17:49:04 -0600 Subject: [PATCH 01/28] Added generic LRUCache and its unit test, still need to figure out if the string in map can be generic --- structs/cache/cache.go | 9 +++ structs/cache/lrucache/lrucache.go | 90 +++++++++++++++++++++++++ structs/cache/lrucache/lrucache_test.go | 85 +++++++++++++++++++++++ 3 files changed, 184 insertions(+) create mode 100644 structs/cache/cache.go create mode 100644 structs/cache/lrucache/lrucache.go create mode 100644 structs/cache/lrucache/lrucache_test.go diff --git a/structs/cache/cache.go b/structs/cache/cache.go new file mode 100644 index 00000000..bfda80b1 --- /dev/null +++ b/structs/cache/cache.go @@ -0,0 +1,9 @@ +package cache + +type Cache interface { + Get() + Put() + Remove() + Clear() + New() +} diff --git a/structs/cache/lrucache/lrucache.go b/structs/cache/lrucache/lrucache.go new file mode 100644 index 00000000..be2fa4c7 --- /dev/null +++ b/structs/cache/lrucache/lrucache.go @@ -0,0 +1,90 @@ +package lrucache + +import ( + "container/list" +) + +type KeyPair struct { + Key string + Value interface{} +} + +type LruCache struct { + Capacity uint + List *list.List + Elements map[string]*list.Element +} + +func NewLruCache(capacity uint) *LruCache { + return &LruCache{capacity, list.New(), make(map[string]*list.Element)} +} + +// Gets an item from the cache using a provided key. Once an item has been +// retrieved, move it to the front of the cache's queue. Return a bool to +// signify a value was found since a key could be mapped to nil. +func (cache *LruCache) Get(key string) (interface{}, bool) { + var value interface{} + var found bool + + if node, ok := cache.Elements[key]; ok { + value = node.Value.(*list.Element).Value.(KeyPair).Value + found = true + cache.List.MoveToFront(node) + } + + return value, found +} + +// Add an item to the cache and move it to the front of the queue. +// If the item's key is already in the cache, update the key's value +// and move the the item to the front of the queue. +func (cache *LruCache) Put(key string, value interface{}) { + // Update key's value if already present in the cache + if node, ok := cache.Elements[key]; ok { + cache.List.MoveToFront(node) + node.Value.(*list.Element).Value = KeyPair{Key: key, Value: value} + + } else { + // Remove least recently used item in cache if the cache's capacity has reached its limit + if uint(cache.List.Len()) >= cache.Capacity { + // Remove node from the cache's internal map + elementToRemove := cache.List.Back().Value.(*list.Element).Value.(KeyPair).Key + delete(cache.Elements, elementToRemove) + + cache.List.Remove(cache.List.Back()) + } + } + + newNode := &list.Element{ + Value: KeyPair{ + Key: key, + Value: value, + }, + } + + mostRecentlyUsed := cache.List.PushFront(newNode) + cache.Elements[key] = mostRecentlyUsed +} + +// Delete an item from the cache based off the key +func (cache *LruCache) Remove(key string) { + if node, ok := cache.Elements[key]; ok { + delete(cache.Elements, key) + cache.List.Remove(node) + } +} + +func (cache *LruCache) Clear() { + cache.Elements = make(map[string]*list.Element) + cache.List.Init() +} + +func (cache *LruCache) GetMostRecentlyUsed() (interface{}, interface{}) { + keyPair := cache.List.Front().Value.(*list.Element).Value.(KeyPair) + return keyPair.Key, keyPair.Value +} + +func (cache *LruCache) GetLeastRecentlyUsed() (interface{}, interface{}) { + keyPair := cache.List.Back().Value.(*list.Element).Value.(KeyPair) + return keyPair.Key, keyPair.Value +} diff --git a/structs/cache/lrucache/lrucache_test.go b/structs/cache/lrucache/lrucache_test.go new file mode 100644 index 00000000..e5dfc3f7 --- /dev/null +++ b/structs/cache/lrucache/lrucache_test.go @@ -0,0 +1,85 @@ +package lrucache + +import ( + "container/list" + "fmt" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +var capacity uint = 5 + +type LruCacheTestSuite struct { + suite.Suite + cache LruCache + assert assert.Assertions +} + +// Initialize a cache and max out its capacity +func (suite *LruCacheTestSuite) SetupTest() { + suite.cache = *NewLruCache(capacity) + for i := 0; i < int(capacity); i++ { + suite.cache.Put(strconv.Itoa(int(i)), i) + } +} + +func (suite *LruCacheTestSuite) TestGetMostRecentlyUsed() { + expectedKey, expectedValue := "4", 4 + key, value := suite.cache.GetMostRecentlyUsed() + + assert.Equal(suite.T(), expectedKey, key) + assert.Equal(suite.T(), expectedValue, value) +} + +func (suite *LruCacheTestSuite) TestGetLeastRecentlyUsed() { + expectedKey, expectedValue := "0", 0 + key, value := suite.cache.GetLeastRecentlyUsed() + + assert.Equal(suite.T(), expectedKey, key) + assert.Equal(suite.T(), expectedValue, value) +} + +func (suite *LruCacheTestSuite) TestRemove() { + toRemove := "2" + _, ok := suite.cache.Get(toRemove) + assert.True(suite.T(), ok, "The element with key %s to be removed from the cache is not in the cache", toRemove) + + suite.cache.Remove(toRemove) + + _, okAfterRemoval := suite.cache.Get(toRemove) + assert.False(suite.T(), okAfterRemoval, "The element with key %s was not removed from the cache", toRemove) +} + +// Check if the values the cache was initialized with can be retrieved +func (suite *LruCacheTestSuite) TestGet() { + for i := 0; i < int(capacity); i++ { + value, ok := suite.cache.Get(strconv.Itoa(int(i))) + assert.True(suite.T(), ok, "The key %d did not exist in the cache", i) + + if ok { + assert.Equal(suite.T(), value, i) + } + } +} + +func (suite *LruCacheTestSuite) TestCapacityExceeded() { + // The first element put in the cache is the least recently used element + // so adding more elements should delete it from the queue + toRemove := "0" + + for e := suite.cache.List.Front(); e != nil; e = e.Next() { + fmt.Println(e.Value.(*list.Element).Value.(KeyPair).Key) + } + + suite.cache.Put(strconv.Itoa(int(capacity)), capacity) + + _, okAfterOverwritten := suite.cache.Get(toRemove) + assert.False(suite.T(), okAfterOverwritten, "The element with key %s was not overwritten in the cache", toRemove) +} + +func TestLruCacheTestSuite(t *testing.T) { + suite.Run(t, new(LruCacheTestSuite)) +} From 07058192497164b11260f4e137cdf4cdcb86a5e4 Mon Sep 17 00:00:00 2001 From: Adam Briles Date: Tue, 16 Aug 2022 11:57:23 -0600 Subject: [PATCH 02/28] Added generic LRUCache and its unit test, still need to figure out if the string in map can be generic --- structs/cache/lrucache/lrucache.go | 1 + structs/cache/lrucache/lrucache_test.go | 1 + 2 files changed, 2 insertions(+) diff --git a/structs/cache/lrucache/lrucache.go b/structs/cache/lrucache/lrucache.go index be2fa4c7..13960c71 100644 --- a/structs/cache/lrucache/lrucache.go +++ b/structs/cache/lrucache/lrucache.go @@ -74,6 +74,7 @@ func (cache *LruCache) Remove(key string) { } } +// Clear all all internal data structures func (cache *LruCache) Clear() { cache.Elements = make(map[string]*list.Element) cache.List.Init() diff --git a/structs/cache/lrucache/lrucache_test.go b/structs/cache/lrucache/lrucache_test.go index e5dfc3f7..9008ec79 100644 --- a/structs/cache/lrucache/lrucache_test.go +++ b/structs/cache/lrucache/lrucache_test.go @@ -66,6 +66,7 @@ func (suite *LruCacheTestSuite) TestGet() { } func (suite *LruCacheTestSuite) TestCapacityExceeded() { + // The first element put in the cache is the least recently used element // so adding more elements should delete it from the queue toRemove := "0" From d5aabfd2a991a177f10255fca5c946cd8f59abf1 Mon Sep 17 00:00:00 2001 From: Adam Briles Date: Tue, 16 Aug 2022 13:47:57 -0600 Subject: [PATCH 03/28] The map taking a generic key gets too awkward, just left it as a string for now. Added a mutex lock --- structs/cache/lrucache/lrucache.go | 33 ++++++++++++++++++++++--- structs/cache/lrucache/lrucache_test.go | 20 ++++++++++----- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/structs/cache/lrucache/lrucache.go b/structs/cache/lrucache/lrucache.go index 13960c71..9df3a79f 100644 --- a/structs/cache/lrucache/lrucache.go +++ b/structs/cache/lrucache/lrucache.go @@ -2,6 +2,7 @@ package lrucache import ( "container/list" + "sync" ) type KeyPair struct { @@ -10,13 +11,18 @@ type KeyPair struct { } type LruCache struct { - Capacity uint - List *list.List - Elements map[string]*list.Element + Capacity uint + List *list.List + CacheMutex sync.Mutex + Elements map[string]*list.Element } func NewLruCache(capacity uint) *LruCache { - return &LruCache{capacity, list.New(), make(map[string]*list.Element)} + return &LruCache{ + Capacity: capacity, + List: list.New(), + Elements: make(map[string]*list.Element), + } } // Gets an item from the cache using a provided key. Once an item has been @@ -26,6 +32,8 @@ func (cache *LruCache) Get(key string) (interface{}, bool) { var value interface{} var found bool + cache.CacheMutex.Lock() + defer cache.CacheMutex.Unlock() if node, ok := cache.Elements[key]; ok { value = node.Value.(*list.Element).Value.(KeyPair).Value found = true @@ -39,6 +47,9 @@ func (cache *LruCache) Get(key string) (interface{}, bool) { // If the item's key is already in the cache, update the key's value // and move the the item to the front of the queue. func (cache *LruCache) Put(key string, value interface{}) { + cache.CacheMutex.Lock() + defer cache.CacheMutex.Unlock() + // Update key's value if already present in the cache if node, ok := cache.Elements[key]; ok { cache.List.MoveToFront(node) @@ -68,6 +79,8 @@ func (cache *LruCache) Put(key string, value interface{}) { // Delete an item from the cache based off the key func (cache *LruCache) Remove(key string) { + cache.CacheMutex.Lock() + defer cache.CacheMutex.Unlock() if node, ok := cache.Elements[key]; ok { delete(cache.Elements, key) cache.List.Remove(node) @@ -76,16 +89,28 @@ func (cache *LruCache) Remove(key string) { // Clear all all internal data structures func (cache *LruCache) Clear() { + cache.CacheMutex.Lock() + defer cache.CacheMutex.Unlock() cache.Elements = make(map[string]*list.Element) cache.List.Init() } func (cache *LruCache) GetMostRecentlyUsed() (interface{}, interface{}) { + cache.CacheMutex.Lock() + defer cache.CacheMutex.Unlock() keyPair := cache.List.Front().Value.(*list.Element).Value.(KeyPair) return keyPair.Key, keyPair.Value } func (cache *LruCache) GetLeastRecentlyUsed() (interface{}, interface{}) { + cache.CacheMutex.Lock() + defer cache.CacheMutex.Unlock() keyPair := cache.List.Back().Value.(*list.Element).Value.(KeyPair) return keyPair.Key, keyPair.Value } + +func (cache *LruCache) GetCurrentCapacity() int { + cache.CacheMutex.Lock() + defer cache.CacheMutex.Unlock() + return cache.List.Len() +} diff --git a/structs/cache/lrucache/lrucache_test.go b/structs/cache/lrucache/lrucache_test.go index 9008ec79..71652ae8 100644 --- a/structs/cache/lrucache/lrucache_test.go +++ b/structs/cache/lrucache/lrucache_test.go @@ -1,8 +1,6 @@ package lrucache import ( - "container/list" - "fmt" "strconv" "testing" @@ -65,22 +63,32 @@ func (suite *LruCacheTestSuite) TestGet() { } } +func (suite *LruCacheTestSuite) TestGetCurrentCapacity() { + assert.Equal(suite.T(), int(capacity), suite.cache.GetCurrentCapacity()) +} + func (suite *LruCacheTestSuite) TestCapacityExceeded() { + // Check that the cache has something in it to start with // The first element put in the cache is the least recently used element // so adding more elements should delete it from the queue toRemove := "0" - for e := suite.cache.List.Front(); e != nil; e = e.Next() { - fmt.Println(e.Value.(*list.Element).Value.(KeyPair).Key) - } - suite.cache.Put(strconv.Itoa(int(capacity)), capacity) _, okAfterOverwritten := suite.cache.Get(toRemove) assert.False(suite.T(), okAfterOverwritten, "The element with key %s was not overwritten in the cache", toRemove) } +func (suite *LruCacheTestSuite) TestClear() { + // Check that the cache has something in it to start with + assert.Equal(suite.T(), int(capacity), suite.cache.GetCurrentCapacity(), "The cache is missing elements. It was not setup properly by SetupTest()") + + suite.cache.Clear() + + assert.Equal(suite.T(), 0, suite.cache.GetCurrentCapacity(), "The cache was not successfully cleared") +} + func TestLruCacheTestSuite(t *testing.T) { suite.Run(t, new(LruCacheTestSuite)) } From f37f1cc65bb8a558e12ed2392c05199da39af648 Mon Sep 17 00:00:00 2001 From: Adam Briles Date: Wed, 17 Aug 2022 10:38:01 -0600 Subject: [PATCH 04/28] changed directory strucutre --- go.mod | 1 - go.sum | 63 +------------------ structs/cache/cache.go | 9 --- structs/{cache => }/lrucache/lrucache.go | 0 structs/{cache => }/lrucache/lrucache_test.go | 0 5 files changed, 1 insertion(+), 72 deletions(-) delete mode 100644 structs/cache/cache.go rename structs/{cache => }/lrucache/lrucache.go (100%) rename structs/{cache => }/lrucache/lrucache_test.go (100%) diff --git a/go.mod b/go.mod index b7ad6aef..3c9fbf99 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,5 @@ require ( github.com/pebbe/zmq4 v1.2.2 github.com/r3labs/diff/v2 v2.14.2 github.com/stretchr/testify v1.5.1 - google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect google.golang.org/protobuf v1.28.0 ) diff --git a/go.sum b/go.sum index 56e76859..92b91547 100644 --- a/go.sum +++ b/go.sum @@ -1,38 +1,14 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/pebbe/zmq4 v1.2.2 h1:RZ5Ogp0D5S6u+tSxopnI3afAf0ifWbvQOAw9HxXvZP4= github.com/pebbe/zmq4 v1.2.2/go.mod h1:7N4y5R18zBiu3l0vajMUWQgZyjv464prE8RCyBcmnZM= 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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/r3labs/diff/v2 v2.14.2 h1:1HVhQKwg1YnoCWzCYlOWYLG4C3yfTudZo5AcrTSgCTc= github.com/r3labs/diff/v2 v2.14.2/go.mod h1:I8noH9Fc2fjSaMxqF3G2lhDdC0b+JXCfyx85tWFM9kc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -41,51 +17,16 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= @@ -93,5 +34,3 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/structs/cache/cache.go b/structs/cache/cache.go deleted file mode 100644 index bfda80b1..00000000 --- a/structs/cache/cache.go +++ /dev/null @@ -1,9 +0,0 @@ -package cache - -type Cache interface { - Get() - Put() - Remove() - Clear() - New() -} diff --git a/structs/cache/lrucache/lrucache.go b/structs/lrucache/lrucache.go similarity index 100% rename from structs/cache/lrucache/lrucache.go rename to structs/lrucache/lrucache.go diff --git a/structs/cache/lrucache/lrucache_test.go b/structs/lrucache/lrucache_test.go similarity index 100% rename from structs/cache/lrucache/lrucache_test.go rename to structs/lrucache/lrucache_test.go From 55b14c0037530fe7f2806f4a01392fe3d2d5c99c Mon Sep 17 00:00:00 2001 From: Adam Briles Date: Thu, 18 Aug 2022 10:51:30 -0600 Subject: [PATCH 05/28] added cache interface back in --- structs/cache/cache.go | 0 structs/{ => cache}/lrucache/lrucache.go | 0 structs/{ => cache}/lrucache/lrucache_test.go | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 structs/cache/cache.go rename structs/{ => cache}/lrucache/lrucache.go (100%) rename structs/{ => cache}/lrucache/lrucache_test.go (100%) diff --git a/structs/cache/cache.go b/structs/cache/cache.go new file mode 100644 index 00000000..e69de29b diff --git a/structs/lrucache/lrucache.go b/structs/cache/lrucache/lrucache.go similarity index 100% rename from structs/lrucache/lrucache.go rename to structs/cache/lrucache/lrucache.go diff --git a/structs/lrucache/lrucache_test.go b/structs/cache/lrucache/lrucache_test.go similarity index 100% rename from structs/lrucache/lrucache_test.go rename to structs/cache/lrucache/lrucache_test.go From 178b7c736d1cf1de32b572a25c88aed765205ee1 Mon Sep 17 00:00:00 2001 From: Adam Briles Date: Thu, 18 Aug 2022 16:59:17 -0600 Subject: [PATCH 06/28] Added cache interface back in --- structs/cache/cache.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/structs/cache/cache.go b/structs/cache/cache.go index e69de29b..d1ea855a 100644 --- a/structs/cache/cache.go +++ b/structs/cache/cache.go @@ -0,0 +1,8 @@ +package cache + +type CacheInterface interface { + Get() (interface{}, bool) + Put() + Clear() + Remove() +} From ae52098588185a8995fe516f2e0f05a60e909ef4 Mon Sep 17 00:00:00 2001 From: Adam Briles Date: Thu, 18 Aug 2022 17:33:42 -0600 Subject: [PATCH 07/28] Added cache interface back in --- structs/cache/cache.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structs/cache/cache.go b/structs/cache/cache.go index d1ea855a..de7703cd 100644 --- a/structs/cache/cache.go +++ b/structs/cache/cache.go @@ -1,7 +1,7 @@ package cache type CacheInterface interface { - Get() (interface{}, bool) + Get(string) (interface{}, bool) Put() Clear() Remove() From e20a6eac15bf97ce329a9099acfecd4a28c0fbe6 Mon Sep 17 00:00:00 2001 From: Adam Briles Date: Thu, 18 Aug 2022 17:35:47 -0600 Subject: [PATCH 08/28] Added cache interface back in --- structs/cache/cache.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structs/cache/cache.go b/structs/cache/cache.go index de7703cd..38b511ab 100644 --- a/structs/cache/cache.go +++ b/structs/cache/cache.go @@ -2,7 +2,7 @@ package cache type CacheInterface interface { Get(string) (interface{}, bool) - Put() + Put(string, interface{}) Clear() Remove() } From 2be150d6eee0295b07050b68f34e6a5780cd5b3f Mon Sep 17 00:00:00 2001 From: Adam Briles Date: Thu, 18 Aug 2022 17:37:39 -0600 Subject: [PATCH 09/28] Added cache interface back in --- structs/cache/cache.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structs/cache/cache.go b/structs/cache/cache.go index 38b511ab..347c10c3 100644 --- a/structs/cache/cache.go +++ b/structs/cache/cache.go @@ -4,5 +4,5 @@ type CacheInterface interface { Get(string) (interface{}, bool) Put(string, interface{}) Clear() - Remove() + Remove(string) } From be533e67d60bbb5863926e249c444a60c3333760 Mon Sep 17 00:00:00 2001 From: Adam Briles Date: Wed, 24 Aug 2022 12:29:38 -0600 Subject: [PATCH 10/28] Added RRCache and unit tests. Added Sweeper interface --- structs/cache/cache.go | 8 -- structs/cache/lrucache/lrucache.go | 116 ------------------------ structs/cache/lrucache/lrucache_test.go | 94 ------------------- util/util.go | 13 +++ 4 files changed, 13 insertions(+), 218 deletions(-) delete mode 100644 structs/cache/cache.go delete mode 100644 structs/cache/lrucache/lrucache.go delete mode 100644 structs/cache/lrucache/lrucache_test.go diff --git a/structs/cache/cache.go b/structs/cache/cache.go deleted file mode 100644 index 347c10c3..00000000 --- a/structs/cache/cache.go +++ /dev/null @@ -1,8 +0,0 @@ -package cache - -type CacheInterface interface { - Get(string) (interface{}, bool) - Put(string, interface{}) - Clear() - Remove(string) -} diff --git a/structs/cache/lrucache/lrucache.go b/structs/cache/lrucache/lrucache.go deleted file mode 100644 index 9df3a79f..00000000 --- a/structs/cache/lrucache/lrucache.go +++ /dev/null @@ -1,116 +0,0 @@ -package lrucache - -import ( - "container/list" - "sync" -) - -type KeyPair struct { - Key string - Value interface{} -} - -type LruCache struct { - Capacity uint - List *list.List - CacheMutex sync.Mutex - Elements map[string]*list.Element -} - -func NewLruCache(capacity uint) *LruCache { - return &LruCache{ - Capacity: capacity, - List: list.New(), - Elements: make(map[string]*list.Element), - } -} - -// Gets an item from the cache using a provided key. Once an item has been -// retrieved, move it to the front of the cache's queue. Return a bool to -// signify a value was found since a key could be mapped to nil. -func (cache *LruCache) Get(key string) (interface{}, bool) { - var value interface{} - var found bool - - cache.CacheMutex.Lock() - defer cache.CacheMutex.Unlock() - if node, ok := cache.Elements[key]; ok { - value = node.Value.(*list.Element).Value.(KeyPair).Value - found = true - cache.List.MoveToFront(node) - } - - return value, found -} - -// Add an item to the cache and move it to the front of the queue. -// If the item's key is already in the cache, update the key's value -// and move the the item to the front of the queue. -func (cache *LruCache) Put(key string, value interface{}) { - cache.CacheMutex.Lock() - defer cache.CacheMutex.Unlock() - - // Update key's value if already present in the cache - if node, ok := cache.Elements[key]; ok { - cache.List.MoveToFront(node) - node.Value.(*list.Element).Value = KeyPair{Key: key, Value: value} - - } else { - // Remove least recently used item in cache if the cache's capacity has reached its limit - if uint(cache.List.Len()) >= cache.Capacity { - // Remove node from the cache's internal map - elementToRemove := cache.List.Back().Value.(*list.Element).Value.(KeyPair).Key - delete(cache.Elements, elementToRemove) - - cache.List.Remove(cache.List.Back()) - } - } - - newNode := &list.Element{ - Value: KeyPair{ - Key: key, - Value: value, - }, - } - - mostRecentlyUsed := cache.List.PushFront(newNode) - cache.Elements[key] = mostRecentlyUsed -} - -// Delete an item from the cache based off the key -func (cache *LruCache) Remove(key string) { - cache.CacheMutex.Lock() - defer cache.CacheMutex.Unlock() - if node, ok := cache.Elements[key]; ok { - delete(cache.Elements, key) - cache.List.Remove(node) - } -} - -// Clear all all internal data structures -func (cache *LruCache) Clear() { - cache.CacheMutex.Lock() - defer cache.CacheMutex.Unlock() - cache.Elements = make(map[string]*list.Element) - cache.List.Init() -} - -func (cache *LruCache) GetMostRecentlyUsed() (interface{}, interface{}) { - cache.CacheMutex.Lock() - defer cache.CacheMutex.Unlock() - keyPair := cache.List.Front().Value.(*list.Element).Value.(KeyPair) - return keyPair.Key, keyPair.Value -} - -func (cache *LruCache) GetLeastRecentlyUsed() (interface{}, interface{}) { - cache.CacheMutex.Lock() - defer cache.CacheMutex.Unlock() - keyPair := cache.List.Back().Value.(*list.Element).Value.(KeyPair) - return keyPair.Key, keyPair.Value -} - -func (cache *LruCache) GetCurrentCapacity() int { - cache.CacheMutex.Lock() - defer cache.CacheMutex.Unlock() - return cache.List.Len() -} diff --git a/structs/cache/lrucache/lrucache_test.go b/structs/cache/lrucache/lrucache_test.go deleted file mode 100644 index 71652ae8..00000000 --- a/structs/cache/lrucache/lrucache_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package lrucache - -import ( - "strconv" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -var capacity uint = 5 - -type LruCacheTestSuite struct { - suite.Suite - cache LruCache - assert assert.Assertions -} - -// Initialize a cache and max out its capacity -func (suite *LruCacheTestSuite) SetupTest() { - suite.cache = *NewLruCache(capacity) - for i := 0; i < int(capacity); i++ { - suite.cache.Put(strconv.Itoa(int(i)), i) - } -} - -func (suite *LruCacheTestSuite) TestGetMostRecentlyUsed() { - expectedKey, expectedValue := "4", 4 - key, value := suite.cache.GetMostRecentlyUsed() - - assert.Equal(suite.T(), expectedKey, key) - assert.Equal(suite.T(), expectedValue, value) -} - -func (suite *LruCacheTestSuite) TestGetLeastRecentlyUsed() { - expectedKey, expectedValue := "0", 0 - key, value := suite.cache.GetLeastRecentlyUsed() - - assert.Equal(suite.T(), expectedKey, key) - assert.Equal(suite.T(), expectedValue, value) -} - -func (suite *LruCacheTestSuite) TestRemove() { - toRemove := "2" - _, ok := suite.cache.Get(toRemove) - assert.True(suite.T(), ok, "The element with key %s to be removed from the cache is not in the cache", toRemove) - - suite.cache.Remove(toRemove) - - _, okAfterRemoval := suite.cache.Get(toRemove) - assert.False(suite.T(), okAfterRemoval, "The element with key %s was not removed from the cache", toRemove) -} - -// Check if the values the cache was initialized with can be retrieved -func (suite *LruCacheTestSuite) TestGet() { - for i := 0; i < int(capacity); i++ { - value, ok := suite.cache.Get(strconv.Itoa(int(i))) - assert.True(suite.T(), ok, "The key %d did not exist in the cache", i) - - if ok { - assert.Equal(suite.T(), value, i) - } - } -} - -func (suite *LruCacheTestSuite) TestGetCurrentCapacity() { - assert.Equal(suite.T(), int(capacity), suite.cache.GetCurrentCapacity()) -} - -func (suite *LruCacheTestSuite) TestCapacityExceeded() { - // Check that the cache has something in it to start with - - // The first element put in the cache is the least recently used element - // so adding more elements should delete it from the queue - toRemove := "0" - - suite.cache.Put(strconv.Itoa(int(capacity)), capacity) - - _, okAfterOverwritten := suite.cache.Get(toRemove) - assert.False(suite.T(), okAfterOverwritten, "The element with key %s was not overwritten in the cache", toRemove) -} - -func (suite *LruCacheTestSuite) TestClear() { - // Check that the cache has something in it to start with - assert.Equal(suite.T(), int(capacity), suite.cache.GetCurrentCapacity(), "The cache is missing elements. It was not setup properly by SetupTest()") - - suite.cache.Clear() - - assert.Equal(suite.T(), 0, suite.cache.GetCurrentCapacity(), "The cache was not successfully cleared") -} - -func TestLruCacheTestSuite(t *testing.T) { - suite.Run(t, new(LruCacheTestSuite)) -} diff --git a/util/util.go b/util/util.go index 7d575a28..daa38e6c 100644 --- a/util/util.go +++ b/util/util.go @@ -9,3 +9,16 @@ func ContainsString(s []string, e string) bool { } return false } + +// Returns a slice of all keys in a map +func GetMapKeys(m map[string]interface{}) []string { + keys := make([]string, len(m)) + + i := 0 + for key := range m { + keys[i] = key + i++ + } + + return keys +} From 353fc1bececf51c91e5ceea7987cba9f0ed0b9b8 Mon Sep 17 00:00:00 2001 From: Adam Briles Date: Wed, 24 Aug 2022 12:29:56 -0600 Subject: [PATCH 11/28] Added RRCache and unit tests. Added Sweeper interface --- structs/cache/cacher/cacher.go | 12 ++ structs/cache/cacher/lrucache.go | 165 ++++++++++++++++++ structs/cache/cacher/lrucache_test.go | 128 ++++++++++++++ .../cache/cacher/randomreplacementcache.go | 122 +++++++++++++ .../cacher/randomreplacementcache_test.go | 94 ++++++++++ structs/cache/sweeper/sweeper.go | 9 + 6 files changed, 530 insertions(+) create mode 100644 structs/cache/cacher/cacher.go create mode 100644 structs/cache/cacher/lrucache.go create mode 100644 structs/cache/cacher/lrucache_test.go create mode 100644 structs/cache/cacher/randomreplacementcache.go create mode 100644 structs/cache/cacher/randomreplacementcache_test.go create mode 100644 structs/cache/sweeper/sweeper.go diff --git a/structs/cache/cacher/cacher.go b/structs/cache/cacher/cacher.go new file mode 100644 index 00000000..6cab4a38 --- /dev/null +++ b/structs/cache/cacher/cacher.go @@ -0,0 +1,12 @@ +package cacher + +// Interface for structs implementing basic caching functionality +type Cacher interface { + Get(string) (interface{}, bool) + Put(string, interface{}) + Clear() + Remove(string) + + // An iterator + NextElement() interface{} +} diff --git a/structs/cache/cacher/lrucache.go b/structs/cache/cacher/lrucache.go new file mode 100644 index 00000000..0ab7b41d --- /dev/null +++ b/structs/cache/cacher/lrucache.go @@ -0,0 +1,165 @@ +package cacher + +import ( + "container/list" + "sync" + + "github.com/untangle/golang-shared/services/logger" +) + +type KeyPair struct { + Key string + Value interface{} +} + +// A simple LRU Cache implementation. The least recently used elements +// in the cache are removed if the cache's max capacity is hit. The cache's +// mutex cannot be a RWMutex since Gets alter the cache's underlying data structures. +// O(1) reads and O(1) insertions. +type LruCache struct { + capacity uint + list *list.List + cacheMutex sync.Mutex + elements map[string]*list.Element + + // Name of your cache. Only used to provide accurate logging + cacheName string +} + +func NewLruCache(capacity uint, cacheName string) *LruCache { + return &LruCache{ + capacity: capacity, + list: list.New(), + elements: make(map[string]*list.Element), + cacheName: cacheName, + } +} + +// Gets an item from the cache using a provided key. Once an item has been +// retrieved, move it to the front of the cache's queue. Return a bool to +// signify a value was found since a key could be mapped to nil. +func (cache *LruCache) Get(key string) (interface{}, bool) { + var value interface{} + var found bool + + cache.cacheMutex.Lock() + defer cache.cacheMutex.Unlock() + if node, ok := cache.elements[key]; ok { + value = node.Value.(*list.Element).Value.(KeyPair).Value + found = true + cache.list.MoveToFront(node) + } + + return value, found +} + +func (cache *LruCache) GetIterator() func() (string, interface{}, bool) { + // Once an iterator has been retrieved, it captures the state of + // of the cache. If the cache is updated the iterator won't contain + // the update + cache.cacheMutex.Lock() + listSnapshot := copyLinkedList(cache.list) + cache.cacheMutex.Unlock() + + node := listSnapshot.Front() + // Return key, val, and if there is anything left to iterate over + return func() (string, interface{}, bool) { + if node != nil { + + currentNode := node + currentKey := currentNode.Value.(*list.Element).Value.(KeyPair).Key + currentValue := currentNode.Value.(*list.Element).Value.(KeyPair).Value + + node = node.Next() + + return currentKey, currentValue, true + } else { + return "", nil, false + } + } +} + +func copyLinkedList(original *list.List) *list.List { + listCopy := list.New() + for node := original.Front(); node != nil; node = node.Next() { + listCopy.PushBack(node.Value) + } + return listCopy +} + +// Add an item to the cache and move it to the front of the queue. +// If the item's key is already in the cache, update the key's value +// and move the the item to the front of the queue. +func (cache *LruCache) Put(key string, value interface{}) { + cache.cacheMutex.Lock() + defer cache.cacheMutex.Unlock() + + // Update key's value if already present in the cache + if node, ok := cache.elements[key]; ok { + cache.list.MoveToFront(node) + node.Value.(*list.Element).Value = KeyPair{Key: key, Value: value} + + logger.Debug("Updated the element with key %s in the cache named %s", key, cache.cacheName) + } else { + // Remove least recently used item in cache if the cache's capacity has reached its limit + if uint(cache.list.Len()) >= cache.capacity { + // Remove node from the cache's internal map + elementToRemove := cache.list.Back().Value.(*list.Element).Value.(KeyPair).Key + delete(cache.elements, elementToRemove) + + cache.list.Remove(cache.list.Back()) + logger.Debug("Removed element with key %s from the cache named %s", key, cache.cacheName) + } + + newNode := &list.Element{ + Value: KeyPair{ + Key: key, + Value: value, + }, + } + + mostRecentlyUsed := cache.list.PushFront(newNode) + cache.elements[key] = mostRecentlyUsed + logger.Debug("Added element with key %s to the cache named %s", key, cache.cacheName) + } +} + +// Delete an item from the cache based off the key +func (cache *LruCache) Remove(key string) { + cache.cacheMutex.Lock() + defer cache.cacheMutex.Unlock() + if node, ok := cache.elements[key]; ok { + delete(cache.elements, key) + cache.list.Remove(node) + logger.Debug("Removed element with key %s from the cache name %s", key, cache.cacheName) + } +} + +// Clear all all internal data structures +func (cache *LruCache) Clear() { + cache.cacheMutex.Lock() + defer cache.cacheMutex.Unlock() + cache.elements = make(map[string]*list.Element) + cache.list.Init() + logger.Debug("Cleared cache of name %s", cache.cacheName) +} + +func (cache *LruCache) GetMostRecentlyUsed() (interface{}, interface{}) { + cache.cacheMutex.Lock() + defer cache.cacheMutex.Unlock() + keyPair := cache.list.Front().Value.(*list.Element).Value.(KeyPair) + return keyPair.Key, keyPair.Value +} + +func (cache *LruCache) GetLeastRecentlyUsed() (interface{}, interface{}) { + cache.cacheMutex.Lock() + defer cache.cacheMutex.Unlock() + keyPair := cache.list.Back().Value.(*list.Element).Value.(KeyPair) + return keyPair.Key, keyPair.Value +} + +func (cache *LruCache) GetCurrentCapacity() int { + cache.cacheMutex.Lock() + defer cache.cacheMutex.Unlock() + return cache.list.Len() +} diff --git a/structs/cache/cacher/lrucache_test.go b/structs/cache/cacher/lrucache_test.go new file mode 100644 index 00000000..38af8e00 --- /dev/null +++ b/structs/cache/cacher/lrucache_test.go @@ -0,0 +1,128 @@ +package cacher + +import ( + "strconv" + "testing" + + "github.com/stretchr/testify/suite" +) + +type LruCacheTestSuite struct { + suite.Suite + cache LruCache + capacity uint + cacheName string +} + +// Initialize a cache and max out its capacity +func (suite *LruCacheTestSuite) SetupTest() { + suite.capacity = 5 + suite.cacheName = "LRUUnitTest" + suite.cache = *NewLruCache(suite.capacity, suite.cacheName) + for i := 0; i < int(suite.capacity); i++ { + suite.cache.Put(strconv.Itoa(int(i)), i) + } +} + +func TestLruCacheTestSuite(t *testing.T) { + suite.Run(t, new(LruCacheTestSuite)) +} + +func (suite *LruCacheTestSuite) TestGetIterator() { + next := suite.cache.GetIterator() + + count := 0 + for key, _, isNext := next(); isNext; key, _, isNext = next() { + _, ok := suite.cache.Get(key) + suite.True(ok, "The iterator returned a value not in the cache") + count += 1 + } + suite.Equal(suite.cache.capacity, uint(count)) +} + +func (suite *LruCacheTestSuite) TestGetMostRecentlyUsed() { + expectedKey, expectedValue := "4", 4 + key, value := suite.cache.GetMostRecentlyUsed() + + suite.Equal(expectedKey, key) + suite.Equal(expectedValue, value) +} + +func (suite *LruCacheTestSuite) TestGetLeastRecentlyUsed() { + expectedKey, expectedValue := "0", 0 + key, value := suite.cache.GetLeastRecentlyUsed() + + suite.Equal(expectedKey, key) + suite.Equal(expectedValue, value) +} + +func (suite *LruCacheTestSuite) TestRemove() { + toRemove := "2" + _, ok := suite.cache.Get(toRemove) + suite.True(ok, "The element with key %s to be removed from the cache is not in the cache", toRemove) + + suite.cache.Remove(toRemove) + + _, okAfterRemoval := suite.cache.Get(toRemove) + suite.False(okAfterRemoval, "The element with key %s was not removed from the cache", toRemove) +} + +// Check if the values the cache was initialized with can be retrieved +func (suite *LruCacheTestSuite) TestGet() { + for i := 0; i < int(suite.capacity); i++ { + value, ok := suite.cache.Get(strconv.Itoa(int(i))) + suite.True(ok, "The key %d did not exist in the cache", i) + + suite.Equal(value, i) + + // Check element was moved to the front of the linked-list + key, value := suite.cache.GetMostRecentlyUsed() + suite.Equal(strconv.Itoa(int(i)), key) + suite.Equal(i, value) + } +} + +func (suite *LruCacheTestSuite) TestGetCurrentCapacity() { + suite.Equal(int(suite.capacity), suite.cache.GetCurrentCapacity()) +} + +func (suite *LruCacheTestSuite) TestCapacityExceeded() { + // Check that the cache has something in it to start with + + // The first element put in the cache is the least recently used element + // so adding more elements should delete it from the queue + toRemove := "0" + + suite.cache.Put(strconv.Itoa(int(suite.capacity)), suite.capacity) + + _, okAfterOverwritten := suite.cache.Get(toRemove) + suite.False(okAfterOverwritten, "The element with key %s was not overwritten in the cache", toRemove) +} + +func (suite *LruCacheTestSuite) TestUpdatingCacheValue() { + toUpdate := "2" + updatedValue := 10 + + _, ok := suite.cache.Get(toUpdate) + suite.True(ok, "The element with key %s to be updated in the cache is not in the cache", toUpdate) + + suite.cache.Put(toUpdate, updatedValue) + + // Check value was updated + value, _ := suite.cache.Get(toUpdate) + suite.Equal(updatedValue, value) + + // Check element was moved to the front of the linked-list + key, value := suite.cache.GetMostRecentlyUsed() + suite.Equal(toUpdate, key) + suite.Equal(updatedValue, value) +} + +func (suite *LruCacheTestSuite) TestClear() { + // Check that the cache has something in it to start with + suite.Equal(int(suite.capacity), suite.cache.GetCurrentCapacity(), "The cache is missing elements. It was not setup properly by SetupTest()") + + suite.cache.Clear() + + suite.Equal(0, suite.cache.GetCurrentCapacity(), "The cache was not successfully cleared") +} diff --git a/structs/cache/cacher/randomreplacementcache.go b/structs/cache/cacher/randomreplacementcache.go new file mode 100644 index 00000000..c206bdbc --- /dev/null +++ b/structs/cache/cacher/randomreplacementcache.go @@ -0,0 +1,122 @@ +package cacher + +import ( + "math/rand" + "sync" + + "github.com/untangle/golang-shared/services/logger" + "github.com/untangle/golang-shared/util" +) + +// Simple cache that removes elements randomly when the cache capacity is met. +// O(1) lookups, but insertions are O(n) when the capacity is met. +// The cache can be read from by multiple threads, but written to by one. +type RandomReplacementCache struct { + maxCapacity uint + elements map[string]interface{} + cacheName string + cacheMutex sync.RWMutex +} + +func NewRandomReplacementCache(capacity uint, cacheName string) *RandomReplacementCache { + return &RandomReplacementCache{ + maxCapacity: capacity, + elements: make(map[string]interface{}), + cacheName: cacheName, + } +} + +func (cache *RandomReplacementCache) GetIterator() func() (string, interface{}, bool) { + // Once an iterator has been retrieved, it captures the state of + // of the cache. If the cache is updated the iterator won't contain + // the update + cache.cacheMutex.RLock() + keys := util.GetMapKeys(cache.elements) + cache.cacheMutex.RUnlock() + + i := 0 + // Return key, val, and if there is anything left to iterate over + return func() (string, interface{}, bool) { + if i == len(keys) { + return "", nil, false + } + + currentKey := keys[i] + + // The value could be nil if the map was altered + value, _ := cache.Get(currentKey) + i += 1 + return currentKey, value, true + } +} + +func (cache *RandomReplacementCache) Get(key string) (interface{}, bool) { + cache.cacheMutex.RLock() + defer cache.cacheMutex.RUnlock() + + value, ok := cache.elements[key] + + return value, ok +} + +func (cache *RandomReplacementCache) getRandomElement() string { + // rand's range is exclusive, and so is range. In order to + // randomly select all elements in the cache, add one to + // the range given to rand + indexToRemove := rand.Intn((len(cache.elements) + 1)) + var keyToRemove string + + count := 0 + for key := range cache.elements { + if count == indexToRemove-1 { + keyToRemove = key + } + + count += 1 + } + + return keyToRemove +} + +func (cache *RandomReplacementCache) Put(key string, value interface{}) { + cache.cacheMutex.Lock() + defer cache.cacheMutex.Unlock() + + // Update element if already present in cache5 + if _, ok := cache.elements[key]; ok { + cache.elements[key] = value + logger.Debug("Updated the element with key %s in the cache named %s", key, cache.cacheName) + } else { + // Remove element if the capacity has been met + if uint(len(cache.elements)) >= cache.maxCapacity { + delete(cache.elements, cache.getRandomElement()) + logger.Debug("Removed element with key %s from the cache named %s", key, cache.cacheName) + } + + // Add new element + cache.elements[key] = value + logger.Debug("Added element with key %s to the cache named %s", key, cache.cacheName) + + } + +} + +func (cache *RandomReplacementCache) Remove(key string) { + cache.cacheMutex.Lock() + defer cache.cacheMutex.Unlock() + delete(cache.elements, key) + logger.Debug("Removed element with key %s from the cache named %s", key, cache.cacheName) +} + +func (cache *RandomReplacementCache) Clear() { + cache.cacheMutex.Lock() + defer cache.cacheMutex.Unlock() + cache.elements = make(map[string]interface{}) + logger.Debug("Cleared cache of name %s", cache.cacheName) +} + +func (cache *RandomReplacementCache) GetCurrentCapacity() int { + cache.cacheMutex.RLock() + defer cache.cacheMutex.RUnlock() + return len(cache.elements) +} diff --git a/structs/cache/cacher/randomreplacementcache_test.go b/structs/cache/cacher/randomreplacementcache_test.go new file mode 100644 index 00000000..80fbd9b9 --- /dev/null +++ b/structs/cache/cacher/randomreplacementcache_test.go @@ -0,0 +1,94 @@ +package cacher + +import ( + "strconv" + "testing" + + "github.com/stretchr/testify/suite" + "github.com/untangle/golang-shared/util" +) + +type RRCacheTestSuite struct { + suite.Suite + cache RandomReplacementCache + capacity uint + cacheName string +} + +func TestRRCacheTestSuite(t *testing.T) { + suite.Run(t, new(RRCacheTestSuite)) +} + +// Initialize a cache and max out its capacity +func (suite *RRCacheTestSuite) SetupTest() { + suite.capacity = 5 + suite.cacheName = "RRUnitTest" + suite.cache = *NewRandomReplacementCache(suite.capacity, suite.cacheName) + + for i := 0; i < int(suite.capacity); i++ { + suite.cache.Put(strconv.Itoa(int(i)), i) + } +} + +func (suite *RRCacheTestSuite) TestNextElement() { + next := suite.cache.GetIterator() + var key string + var isNext bool + + count := 0 + for key, _, isNext = next(); isNext == true; key, _, isNext = next() { + _, ok := suite.cache.elements[key] + + suite.True(ok, "The iterator retrieved a value not in the cache") + count += 1 + } + + suite.Equal(suite.capacity, uint(count), "The iterator did not iterate over ever element in the cache") + +} + +func (suite *RRCacheTestSuite) TestGet() { + for i := 0; i < int(suite.capacity); i++ { + value, ok := suite.cache.Get(strconv.Itoa(i)) + suite.True(ok, "The key %d did not exist in the cache", i) + + suite.Equal(value, i) + } +} + +func (suite *RRCacheTestSuite) TestUpdatingCacheValue() { + toUpdate := "2" + updatedValue := 10 + + _, ok := suite.cache.Get(toUpdate) + suite.True(ok, "The element with key %s to be updated in the cache is not in the cache", toUpdate) + + suite.cache.Put(toUpdate, updatedValue) + + // Check value was updated + value, _ := suite.cache.Get(toUpdate) + suite.Equal(updatedValue, value) +} + +func (suite *RRCacheTestSuite) TestClear() { + suite.Equal(int(suite.capacity), suite.cache.GetCurrentCapacity(), "The cache is missing elements. It was not setup properly by SetupTest()") + + suite.cache.Clear() + + suite.Equal(0, suite.cache.GetCurrentCapacity(), "The cache was not successfully cleared") +} + +func (suite *RRCacheTestSuite) TestCapacityExceeded() { + keysBeforePut := util.GetMapKeys(suite.cache.elements) + + suite.cache.Put("6", 6) + keysAfterPut := util.GetMapKeys(suite.cache.elements) + + // Check if the size is the same, and that the list has changed + suite.Equal(int(suite.capacity), len(suite.cache.elements)) + suite.NotEqual(keysAfterPut, keysBeforePut) +} + +func (suite *RRCacheTestSuite) TestGetCurrentCapacity() { + suite.Equal(int(suite.capacity), suite.cache.GetCurrentCapacity()) +} diff --git a/structs/cache/sweeper/sweeper.go b/structs/cache/sweeper/sweeper.go new file mode 100644 index 00000000..c07682b2 --- /dev/null +++ b/structs/cache/sweeper/sweeper.go @@ -0,0 +1,9 @@ +package sweeper + +// Interface for altering/removing elements from a cache +// StartSweep should kickoff a goroutine that scans the a +// cache on a set interval. +type Sweeper interface { + StartSweep(int) + Remove(interface{}, interface{}) bool +} From 348c38dc4955dba36833d5197d11cf81053ae075 Mon Sep 17 00:00:00 2001 From: Adam Briles Date: Wed, 24 Aug 2022 12:53:35 -0600 Subject: [PATCH 12/28] Fixed function name --- structs/cache/cacher/cacher.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structs/cache/cacher/cacher.go b/structs/cache/cacher/cacher.go index 6cab4a38..497a1811 100644 --- a/structs/cache/cacher/cacher.go +++ b/structs/cache/cacher/cacher.go @@ -8,5 +8,5 @@ type Cacher interface { Remove(string) // An iterator - NextElement() interface{} + GetIterator() interface{} } From 8caed6db1b6e9d135ef55ee30e7d828d21f67a54 Mon Sep 17 00:00:00 2001 From: Adam Briles Date: Wed, 24 Aug 2022 12:57:18 -0600 Subject: [PATCH 13/28] Fixed function name --- structs/cache/cacher/cacher.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structs/cache/cacher/cacher.go b/structs/cache/cacher/cacher.go index 497a1811..024a270b 100644 --- a/structs/cache/cacher/cacher.go +++ b/structs/cache/cacher/cacher.go @@ -8,5 +8,5 @@ type Cacher interface { Remove(string) // An iterator - GetIterator() interface{} + GetIterator() func() (string, interface{}, bool) } From 2610267a702cc2390c744dae26bb318c93d07a66 Mon Sep 17 00:00:00 2001 From: Adam Briles Date: Thu, 25 Aug 2022 16:10:51 -0600 Subject: [PATCH 14/28] Added struct that combines caches and sweepers, and a bunch of unit tests --- structs/cache/cacher/cacher.go | 2 +- structs/cache/cacher/lrucache.go | 6 +++--- structs/cache/cacher/randomreplacementcache.go | 6 +++--- structs/cache/sweeper/sweeper.go | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/structs/cache/cacher/cacher.go b/structs/cache/cacher/cacher.go index 024a270b..6c9199cf 100644 --- a/structs/cache/cacher/cacher.go +++ b/structs/cache/cacher/cacher.go @@ -8,5 +8,5 @@ type Cacher interface { Remove(string) // An iterator - GetIterator() func() (string, interface{}, bool) + GetIterator() func() (string, *interface{}, bool) } diff --git a/structs/cache/cacher/lrucache.go b/structs/cache/cacher/lrucache.go index 0ab7b41d..bde4f410 100644 --- a/structs/cache/cacher/lrucache.go +++ b/structs/cache/cacher/lrucache.go @@ -53,7 +53,7 @@ func (cache *LruCache) Get(key string) (interface{}, bool) { return value, found } -func (cache *LruCache) GetIterator() func() (string, interface{}, bool) { +func (cache *LruCache) GetIterator() func() (string, *interface{}, bool) { // Once an iterator has been retrieved, it captures the state of // of the cache. If the cache is updated the iterator won't contain // the update @@ -63,7 +63,7 @@ func (cache *LruCache) GetIterator() func() (string, interface{}, bool) { node := listSnapshot.Front() // Return key, val, and if there is anything left to iterate over - return func() (string, interface{}, bool) { + return func() (string, *interface{}, bool) { if node != nil { currentNode := node @@ -72,7 +72,7 @@ func (cache *LruCache) GetIterator() func() (string, interface{}, bool) { node = node.Next() - return currentKey, currentValue, true + return currentKey, ¤tValue, true } else { return "", nil, false } diff --git a/structs/cache/cacher/randomreplacementcache.go b/structs/cache/cacher/randomreplacementcache.go index c206bdbc..5232c950 100644 --- a/structs/cache/cacher/randomreplacementcache.go +++ b/structs/cache/cacher/randomreplacementcache.go @@ -26,7 +26,7 @@ func NewRandomReplacementCache(capacity uint, cacheName string) *RandomReplaceme } } -func (cache *RandomReplacementCache) GetIterator() func() (string, interface{}, bool) { +func (cache *RandomReplacementCache) GetIterator() func() (string, *interface{}, bool) { // Once an iterator has been retrieved, it captures the state of // of the cache. If the cache is updated the iterator won't contain // the update @@ -36,7 +36,7 @@ func (cache *RandomReplacementCache) GetIterator() func() (string, interface{}, i := 0 // Return key, val, and if there is anything left to iterate over - return func() (string, interface{}, bool) { + return func() (string, *interface{}, bool) { if i == len(keys) { return "", nil, false } @@ -46,7 +46,7 @@ func (cache *RandomReplacementCache) GetIterator() func() (string, interface{}, // The value could be nil if the map was altered value, _ := cache.Get(currentKey) i += 1 - return currentKey, value, true + return currentKey, &value, true } } diff --git a/structs/cache/sweeper/sweeper.go b/structs/cache/sweeper/sweeper.go index c07682b2..34325188 100644 --- a/structs/cache/sweeper/sweeper.go +++ b/structs/cache/sweeper/sweeper.go @@ -4,6 +4,6 @@ package sweeper // StartSweep should kickoff a goroutine that scans the a // cache on a set interval. type Sweeper interface { - StartSweep(int) - Remove(interface{}, interface{}) bool + StartSweeping(func()) + StopSweeping() } From ac01e93bcaea5a29d8c10d54823a6f8c629a57c9 Mon Sep 17 00:00:00 2001 From: Adam Briles Date: Thu, 25 Aug 2022 16:13:18 -0600 Subject: [PATCH 15/28] added files I meant to add last time... --- structs/cache/sweeper/sweepontime.go | 47 ++++++++++++++ structs/cache/sweeper/sweepontime_test.go | 61 ++++++++++++++++++ structs/cache/sweptcache.go | 48 ++++++++++++++ structs/cache/sweptcache_test.go | 77 +++++++++++++++++++++++ 4 files changed, 233 insertions(+) create mode 100644 structs/cache/sweeper/sweepontime.go create mode 100644 structs/cache/sweeper/sweepontime_test.go create mode 100644 structs/cache/sweptcache.go create mode 100644 structs/cache/sweptcache_test.go diff --git a/structs/cache/sweeper/sweepontime.go b/structs/cache/sweeper/sweepontime.go new file mode 100644 index 00000000..dfb2e2fe --- /dev/null +++ b/structs/cache/sweeper/sweepontime.go @@ -0,0 +1,47 @@ +package sweeper + +import ( + "time" + + "github.com/untangle/golang-shared/services/logger" +) + +type SweepOnTime struct { + shutdownChannel chan bool + waitTime time.Duration +} + +func NewSweepOnTime(waitTime time.Duration) *SweepOnTime { + return &SweepOnTime{ + shutdownChannel: make(chan bool), + waitTime: waitTime, + } +} + +func (sweeper *SweepOnTime) StartSweeping(cleanupFunc func()) { + go sweeper.runCleanup(cleanupFunc) +} + +func (sweeper *SweepOnTime) StopSweeping() { + sweeper.shutdownChannel <- true + + select { + case <-sweeper.shutdownChannel: + logger.Info("Successful shutdown of clean up \n") + case <-time.After(10 * time.Second): + logger.Warn("Failed to properly shutdown cleanupTask\n") + } +} + +func (sweeper *SweepOnTime) runCleanup(cleanupFunc func()) { + for { + select { + case <-sweeper.shutdownChannel: + sweeper.shutdownChannel <- true + return + case <-time.After(sweeper.waitTime): + + cleanupFunc() + } + } +} diff --git a/structs/cache/sweeper/sweepontime_test.go b/structs/cache/sweeper/sweepontime_test.go new file mode 100644 index 00000000..e6a71d51 --- /dev/null +++ b/structs/cache/sweeper/sweepontime_test.go @@ -0,0 +1,61 @@ +package sweeper + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/suite" +) + +type SweepOnTimeTestSuite struct { + suite.Suite + sweeper SweepOnTime + sweepInterval time.Duration + + // Cache used to test if the cleanup task gets run + cache map[string]int + + // A value used as sweep criteria + cacheMaxValue int +} + +func (suite *SweepOnTimeTestSuite) getCleanupFunc() func() { + cleanupCache := &(suite.cache) + elementMaxValue := suite.cacheMaxValue + + // Simple function to remove a cache element if it's over a value + return func() { + for key, val := range *cleanupCache { + if val < elementMaxValue { + delete(*cleanupCache, key) + } + } + } +} + +func TestSweepOnTimeTestSuite(t *testing.T) { + suite.Run(t, new(SweepOnTimeTestSuite)) +} + +func (suite *SweepOnTimeTestSuite) SetupTest() { + suite.sweepInterval = 5 * time.Millisecond + suite.cache = map[string]int{"1": 1, "2": 2, "3": 3, "4": 4} + suite.cacheMaxValue = 3 + suite.sweeper = *NewSweepOnTime(suite.sweepInterval) + + suite.sweeper.StartSweeping(suite.getCleanupFunc()) +} + +func (suite *SweepOnTimeTestSuite) TestSwept() { + time.Sleep((suite.sweepInterval) + 1) + + for key, val := range suite.cache { + fmt.Println(key) + suite.True(val >= suite.cacheMaxValue, "The test cache was not swept") + } +} + +func (suite *SweepOnTimeTestSuite) TearDownTest() { + suite.sweeper.StopSweeping() +} diff --git a/structs/cache/sweptcache.go b/structs/cache/sweptcache.go new file mode 100644 index 00000000..166b64f0 --- /dev/null +++ b/structs/cache/sweptcache.go @@ -0,0 +1,48 @@ +package cache + +import ( + "github.com/untangle/golang-shared/structs/cache/cacher" + "github.com/untangle/golang-shared/structs/cache/sweeper" +) + +// Cache with a struct responsible for kicking off sweeps of the cache. +type SweptCache struct { + cacher.Cacher + + sweeper sweeper.Sweeper +} + +// The sweeper runs a function when certain criteria is met. To give the function being run by the sweeper +// access to the cache, use a closure. +func (sweptCache *SweptCache) generateCleanupTask(cleanupFunc func(string, *interface{}) bool) func() { + + cache := &sweptCache.Cacher + + return func() { + + getNext := (*cache).GetIterator() + + for key, value, ok := getNext(); ok; key, value, ok = getNext() { + if cleanupFunc(key, value) { + sweptCache.Cacher.Remove(key) + } + } + } +} + +func NewSweptCache(cache cacher.Cacher, sweeper sweeper.Sweeper) *SweptCache { + + return &SweptCache{cache, sweeper} +} + +// Starts sweeping the cache. The function provided will be run on every element in the cache +// once a sweep is triggered. +// The key of the cache element, and a pointer to it, must be handled by the provided function. +// If false is returned from the provided function, the cache element will be removed. +func (sweptCache *SweptCache) StartSweeping(cleanupFunc func(string, *interface{}) bool) { + sweptCache.sweeper.StartSweeping(sweptCache.generateCleanupTask(cleanupFunc)) +} + +func (sweptCache *SweptCache) StopSweeping() { + sweptCache.sweeper.StopSweeping() +} diff --git a/structs/cache/sweptcache_test.go b/structs/cache/sweptcache_test.go new file mode 100644 index 00000000..4aae1e39 --- /dev/null +++ b/structs/cache/sweptcache_test.go @@ -0,0 +1,77 @@ +package cache + +import ( + "fmt" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/suite" + "github.com/untangle/golang-shared/structs/cache/cacher" + "github.com/untangle/golang-shared/structs/cache/sweeper" +) + +type TestSweptCache struct { + suite.Suite + sweptCache SweptCache + + sweepInterval int +} + +func TestSweptCacheSuite(t *testing.T) { + suite.Run(t, &TestSweptCache{}) +} + +func (suite *TestSweptCache) SetupTest() { + suite.sweepInterval = 1 + suite.sweptCache = *NewSweptCache(cacher.NewRandomReplacementCache(5, "sweptCacheTest"), sweeper.NewSweepOnTime(time.Duration(suite.sweepInterval)*time.Second)) + + for i := 0; i < 5; i++ { + suite.sweptCache.Put(strconv.Itoa(int(i)), i) + } +} + +func (suite *TestSweptCache) TestElementDeletionFunction() { + + // Remove elements with a value less than 3. Run every second + suite.sweptCache.StartSweeping(func(s string, i *interface{}) bool { + deleteElement := false + if (*i).(int) < 3 { + deleteElement = true + } + + return deleteElement + }) + + time.Sleep((time.Duration(suite.sweepInterval) + 1) * time.Second) + + next := suite.sweptCache.GetIterator() + + for key, val, ok := next(); ok; key, val, ok = next() { + suite.True((*val).(int) >= 3, "The key %s was not swept as expected", key) + } + +} + +func (suite *TestSweptCache) TestElementMutationFunction() { + suite.sweptCache.StartSweeping(func(s string, i *interface{}) bool { + if (*i).(int) < 4 { + (*i) = 4 + } + + return false + }) + + time.Sleep((time.Duration(suite.sweepInterval) + 2) * time.Second) + + next := suite.sweptCache.GetIterator() + for key, val, ok := next(); ok; key, val, ok = next() { + fmt.Printf("%s %d\n", key, *val) + suite.True((*val).(int) == 5, "The key %s was not altered as expected", key) + } + +} + +func (suite *TestSweptCache) TearDownTest() { + suite.sweptCache.StopSweeping() +} From e2f2ed1368c28c2d276ecf336c1d8c7b18101121 Mon Sep 17 00:00:00 2001 From: Adam Briles Date: Sun, 28 Aug 2022 11:58:02 -0600 Subject: [PATCH 16/28] Maybe giving up on having a generic swept cache for now --- structs/cache/sweptcache.go | 1 + 1 file changed, 1 insertion(+) diff --git a/structs/cache/sweptcache.go b/structs/cache/sweptcache.go index 166b64f0..5da9745b 100644 --- a/structs/cache/sweptcache.go +++ b/structs/cache/sweptcache.go @@ -27,6 +27,7 @@ func (sweptCache *SweptCache) generateCleanupTask(cleanupFunc func(string, *inte sweptCache.Cacher.Remove(key) } } + } } From 7379f29e6fbfda718de6c2fef3b1c4eb114e6a10 Mon Sep 17 00:00:00 2001 From: Adam Briles Date: Mon, 29 Aug 2022 13:49:34 -0600 Subject: [PATCH 17/28] RRCache is now O(1) for insertions --- structs/cache/cacher/cacher.go | 5 +- .../cache/cacher/randomreplacementcache.go | 98 +++++++++++++------ .../cacher/randomreplacementcache_test.go | 13 +++ structs/cache/sweptcache.go | 8 +- structs/cache/sweptcache_test.go | 20 ++-- 5 files changed, 100 insertions(+), 44 deletions(-) diff --git a/structs/cache/cacher/cacher.go b/structs/cache/cacher/cacher.go index 6c9199cf..c82f6b8a 100644 --- a/structs/cache/cacher/cacher.go +++ b/structs/cache/cacher/cacher.go @@ -8,5 +8,8 @@ type Cacher interface { Remove(string) // An iterator - GetIterator() func() (string, *interface{}, bool) + GetIterator() func() (string, interface{}, bool) + + // Runs a given function on each cache element + ForEach(func(string, interface{}) bool) } diff --git a/structs/cache/cacher/randomreplacementcache.go b/structs/cache/cacher/randomreplacementcache.go index 5232c950..eb09a908 100644 --- a/structs/cache/cacher/randomreplacementcache.go +++ b/structs/cache/cacher/randomreplacementcache.go @@ -8,25 +8,52 @@ import ( "github.com/untangle/golang-shared/util" ) +// Used to signal that an index should be ignored in the list of keys +const badKeySignifier string = "badKey" + // Simple cache that removes elements randomly when the cache capacity is met. -// O(1) lookups, but insertions are O(n) when the capacity is met. +// O(1) lookups and insertions. For O(1) insertions, size complexity had to be increased. +// The size of the cache will grow for every cache deletion since the keys slice can't +// have elements removed from it. // The cache can be read from by multiple threads, but written to by one. type RandomReplacementCache struct { maxCapacity uint elements map[string]interface{} cacheName string cacheMutex sync.RWMutex + keys []string + keyToIndex map[string]int } func NewRandomReplacementCache(capacity uint, cacheName string) *RandomReplacementCache { return &RandomReplacementCache{ maxCapacity: capacity, - elements: make(map[string]interface{}), + elements: make(map[string]interface{}, capacity), cacheName: cacheName, + + // Removing elements from the keys slice would cause an entire update of the + // keyToIndex map. For a performance bump, just set removed element's keys to + // nil when removed. Since keys capacity will exceed that of the maps, give it + // a much larger size to avoid too many copies + keys: make([]string, 2*capacity), + keyToIndex: make(map[string]int, capacity), } } -func (cache *RandomReplacementCache) GetIterator() func() (string, *interface{}, bool) { +func (cache *RandomReplacementCache) ForEach(cleanupFunc func(string, interface{}) bool) { + cache.cacheMutex.Lock() + defer cache.cacheMutex.Unlock() + + for key, val := range cache.elements { + // Remove element if the cleanUp func returns true + if cleanupFunc(key, val) { + cache.removeElement(key) + } + } + +} + +func (cache *RandomReplacementCache) GetIterator() func() (string, interface{}, bool) { // Once an iterator has been retrieved, it captures the state of // of the cache. If the cache is updated the iterator won't contain // the update @@ -36,7 +63,7 @@ func (cache *RandomReplacementCache) GetIterator() func() (string, *interface{}, i := 0 // Return key, val, and if there is anything left to iterate over - return func() (string, *interface{}, bool) { + return func() (string, interface{}, bool) { if i == len(keys) { return "", nil, false } @@ -59,59 +86,72 @@ func (cache *RandomReplacementCache) Get(key string) (interface{}, bool) { return value, ok } -func (cache *RandomReplacementCache) getRandomElement() string { - // rand's range is exclusive, and so is range. In order to - // randomly select all elements in the cache, add one to - // the range given to rand - indexToRemove := rand.Intn((len(cache.elements) + 1)) - var keyToRemove string - - count := 0 - for key := range cache.elements { - if count == indexToRemove-1 { - keyToRemove = key - } - - count += 1 - } - - return keyToRemove -} - func (cache *RandomReplacementCache) Put(key string, value interface{}) { cache.cacheMutex.Lock() defer cache.cacheMutex.Unlock() - // Update element if already present in cache5 + // Update element if already present in cache if _, ok := cache.elements[key]; ok { cache.elements[key] = value logger.Debug("Updated the element with key %s in the cache named %s", key, cache.cacheName) } else { - // Remove element if the capacity has been met + // Remove element randomly if the capacity has been met if uint(len(cache.elements)) >= cache.maxCapacity { - delete(cache.elements, cache.getRandomElement()) + cache.removeElement(cache.getRandomKeyForRemoval()) logger.Debug("Removed element with key %s from the cache named %s", key, cache.cacheName) } // Add new element cache.elements[key] = value + keyIndex := len(cache.elements) + cache.keys = append(cache.keys, key) + cache.keyToIndex[key] = keyIndex + logger.Debug("Added element with key %s to the cache named %s", key, cache.cacheName) } +} + +// This should never be called on an empty cache +func (cache *RandomReplacementCache) getRandomKeyForRemoval() string { + keyForRemoval := badKeySignifier + for keyForRemoval == badKeySignifier { + keyForRemoval = cache.keys[rand.Intn(len(cache.keys))] + } + + return keyForRemoval +} + +// Don't just use the public function to remove elements since Put/Remove both +// need the write lock. Put calling Remove would result in a deadlock, so +// use this function that they can both call after acquiring the cache's mutex +func (cache *RandomReplacementCache) removeElement(key string) { + if indexToRemove, ok := cache.keyToIndex[key]; ok { + // Deleting keys from the keys list would alter what they're mapped to in keyToIndex + // Instead, set them to a nonsense value to signify they've been removed + cache.keys[indexToRemove] = badKeySignifier + + delete(cache.keyToIndex, key) + delete(cache.elements, key) + + logger.Debug("Removed element with key %s from the cache named %s", key, cache.cacheName) + } } func (cache *RandomReplacementCache) Remove(key string) { cache.cacheMutex.Lock() defer cache.cacheMutex.Unlock() - delete(cache.elements, key) - logger.Debug("Removed element with key %s from the cache named %s", key, cache.cacheName) + cache.removeElement(key) + // else the key didn't exists in the cache and nothing should be done } func (cache *RandomReplacementCache) Clear() { cache.cacheMutex.Lock() defer cache.cacheMutex.Unlock() - cache.elements = make(map[string]interface{}) + cache.elements = make(map[string]interface{}, cache.maxCapacity) + cache.keyToIndex = make(map[string]int, cache.maxCapacity) + cache.keys = make([]string, cache.maxCapacity) logger.Debug("Cleared cache of name %s", cache.cacheName) } diff --git a/structs/cache/cacher/randomreplacementcache_test.go b/structs/cache/cacher/randomreplacementcache_test.go index 80fbd9b9..eb3e5d3d 100644 --- a/structs/cache/cacher/randomreplacementcache_test.go +++ b/structs/cache/cacher/randomreplacementcache_test.go @@ -92,3 +92,16 @@ func (suite *RRCacheTestSuite) TestCapacityExceeded() { func (suite *RRCacheTestSuite) TestGetCurrentCapacity() { suite.Equal(int(suite.capacity), suite.cache.GetCurrentCapacity()) } + +func (suite *RRCacheTestSuite) TestRemove() { + keyToRemove := "2" + _, ok := suite.cache.Get(keyToRemove) + suite.True(ok, "The key -- %s -- going to be removed wasn't in the cache at the start of the test", keyToRemove) + + suite.cache.Remove("2") + + _, ok = suite.cache.Get(keyToRemove) + suite.False(ok, "The key -- %s -- remained in the cache after being removed", keyToRemove) + suite.Equal(suite.cache.maxCapacity-1, uint(len(suite.cache.elements))) + suite.Equal(suite.cache.maxCapacity-1, uint(len(suite.cache.keyToIndex))) +} diff --git a/structs/cache/sweptcache.go b/structs/cache/sweptcache.go index 5da9745b..5851a154 100644 --- a/structs/cache/sweptcache.go +++ b/structs/cache/sweptcache.go @@ -14,13 +14,13 @@ type SweptCache struct { // The sweeper runs a function when certain criteria is met. To give the function being run by the sweeper // access to the cache, use a closure. -func (sweptCache *SweptCache) generateCleanupTask(cleanupFunc func(string, *interface{}) bool) func() { +func (sweptCache *SweptCache) generateCleanupTask(cleanupFunc func(string, interface{}) bool) func() { - cache := &sweptCache.Cacher + cache := sweptCache.Cacher return func() { - getNext := (*cache).GetIterator() + getNext := cache.GetIterator() for key, value, ok := getNext(); ok; key, value, ok = getNext() { if cleanupFunc(key, value) { @@ -40,7 +40,7 @@ func NewSweptCache(cache cacher.Cacher, sweeper sweeper.Sweeper) *SweptCache { // once a sweep is triggered. // The key of the cache element, and a pointer to it, must be handled by the provided function. // If false is returned from the provided function, the cache element will be removed. -func (sweptCache *SweptCache) StartSweeping(cleanupFunc func(string, *interface{}) bool) { +func (sweptCache *SweptCache) StartSweeping(cleanupFunc func(string, interface{}) bool) { sweptCache.sweeper.StartSweeping(sweptCache.generateCleanupTask(cleanupFunc)) } diff --git a/structs/cache/sweptcache_test.go b/structs/cache/sweptcache_test.go index 4aae1e39..8ac015d8 100644 --- a/structs/cache/sweptcache_test.go +++ b/structs/cache/sweptcache_test.go @@ -1,7 +1,6 @@ package cache import ( - "fmt" "strconv" "testing" "time" @@ -27,16 +26,17 @@ func (suite *TestSweptCache) SetupTest() { suite.sweptCache = *NewSweptCache(cacher.NewRandomReplacementCache(5, "sweptCacheTest"), sweeper.NewSweepOnTime(time.Duration(suite.sweepInterval)*time.Second)) for i := 0; i < 5; i++ { - suite.sweptCache.Put(strconv.Itoa(int(i)), i) + newVal := i + suite.sweptCache.Put(strconv.Itoa(int(i)), &newVal) } } func (suite *TestSweptCache) TestElementDeletionFunction() { // Remove elements with a value less than 3. Run every second - suite.sweptCache.StartSweeping(func(s string, i *interface{}) bool { + suite.sweptCache.StartSweeping(func(s string, i interface{}) bool { deleteElement := false - if (*i).(int) < 3 { + if *(*(i.(*interface{}))).(*int) < 3 { deleteElement = true } @@ -48,15 +48,15 @@ func (suite *TestSweptCache) TestElementDeletionFunction() { next := suite.sweptCache.GetIterator() for key, val, ok := next(); ok; key, val, ok = next() { - suite.True((*val).(int) >= 3, "The key %s was not swept as expected", key) + suite.True(*(*(val.(*interface{}))).(*int) >= 3, "The key %s was not swept as expected", key) } } func (suite *TestSweptCache) TestElementMutationFunction() { - suite.sweptCache.StartSweeping(func(s string, i *interface{}) bool { - if (*i).(int) < 4 { - (*i) = 4 + suite.sweptCache.StartSweeping(func(s string, i interface{}) bool { + if *(*(i.(*interface{}))).(*int) < 4 { + *(*(i.(*interface{}))).(*int) = 4 } return false @@ -66,8 +66,8 @@ func (suite *TestSweptCache) TestElementMutationFunction() { next := suite.sweptCache.GetIterator() for key, val, ok := next(); ok; key, val, ok = next() { - fmt.Printf("%s %d\n", key, *val) - suite.True((*val).(int) == 5, "The key %s was not altered as expected", key) + + suite.True(*(*(val.(*interface{}))).(*int) == 4, "The key %s was not altered as expected", key) } } From dac783f1558643cc39a43af9b4da0bd9f7fe2f31 Mon Sep 17 00:00:00 2001 From: Adam Briles Date: Mon, 29 Aug 2022 13:52:59 -0600 Subject: [PATCH 18/28] RRCache is now O(1) for insertions --- structs/cache/cacher/randomreplacementcache.go | 2 +- structs/cache/cacher/randomreplacementcache_test.go | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/structs/cache/cacher/randomreplacementcache.go b/structs/cache/cacher/randomreplacementcache.go index eb09a908..122a2f3c 100644 --- a/structs/cache/cacher/randomreplacementcache.go +++ b/structs/cache/cacher/randomreplacementcache.go @@ -12,7 +12,7 @@ import ( const badKeySignifier string = "badKey" // Simple cache that removes elements randomly when the cache capacity is met. -// O(1) lookups and insertions. For O(1) insertions, size complexity had to be increased. +// O(1) lookups and insertions. For O(1) insertions, space complexity had to be increased. // The size of the cache will grow for every cache deletion since the keys slice can't // have elements removed from it. // The cache can be read from by multiple threads, but written to by one. diff --git a/structs/cache/cacher/randomreplacementcache_test.go b/structs/cache/cacher/randomreplacementcache_test.go index eb3e5d3d..2b4c0ccc 100644 --- a/structs/cache/cacher/randomreplacementcache_test.go +++ b/structs/cache/cacher/randomreplacementcache_test.go @@ -86,6 +86,11 @@ func (suite *RRCacheTestSuite) TestCapacityExceeded() { // Check if the size is the same, and that the list has changed suite.Equal(int(suite.capacity), len(suite.cache.elements)) + suite.Equal(suite.cache.maxCapacity, uint(len(suite.cache.keyToIndex))) + + // The keys slice grew since its elements can't be removed + suite.True(len(suite.cache.keys) > int(suite.cache.maxCapacity)) + suite.NotEqual(keysAfterPut, keysBeforePut) } From 6ad28fa8cd70c9b01f2cabb4f7bb3f49ce73c8cc Mon Sep 17 00:00:00 2001 From: Adam Briles Date: Mon, 29 Aug 2022 15:41:34 -0600 Subject: [PATCH 19/28] RRCache with O(n) deletions and mid point on sweeper location swap --- structs/cache/cacher/lrucache.go | 26 +++++-- .../cache/cacher/randomreplacementcache.go | 71 +++++++++---------- .../cacher/randomreplacementcache_test.go | 2 - 3 files changed, 54 insertions(+), 45 deletions(-) diff --git a/structs/cache/cacher/lrucache.go b/structs/cache/cacher/lrucache.go index bde4f410..8ead7327 100644 --- a/structs/cache/cacher/lrucache.go +++ b/structs/cache/cacher/lrucache.go @@ -35,6 +35,19 @@ func NewLruCache(capacity uint, cacheName string) *LruCache { } } +func (cache *LruCache) ForEach(cleanupFunc func(string, interface{}) bool) { + cache.cacheMutex.Lock() + defer cache.cacheMutex.Unlock() + + for key, val := range cache.elements { + // Remove element if the cleanUp func returns true + if cleanupFunc(key, val) { + cache.removeElement(key) + } + } + +} + // Gets an item from the cache using a provided key. Once an item has been // retrieved, move it to the front of the cache's queue. Return a bool to // signify a value was found since a key could be mapped to nil. @@ -124,10 +137,8 @@ func (cache *LruCache) Put(key string, value interface{}) { } } -// Delete an item from the cache based off the key -func (cache *LruCache) Remove(key string) { - cache.cacheMutex.Lock() - defer cache.cacheMutex.Unlock() +// Does NOT take the cache's lock. Functions calling removeElement() need to +func (cache *LruCache) removeElement(key string) { if node, ok := cache.elements[key]; ok { delete(cache.elements, key) cache.list.Remove(node) @@ -135,6 +146,13 @@ func (cache *LruCache) Remove(key string) { } } +// Delete an item from the cache based off the key +func (cache *LruCache) Remove(key string) { + cache.cacheMutex.Lock() + defer cache.cacheMutex.Unlock() + cache.removeElement(key) +} + // Clear all all internal data structures func (cache *LruCache) Clear() { cache.cacheMutex.Lock() diff --git a/structs/cache/cacher/randomreplacementcache.go b/structs/cache/cacher/randomreplacementcache.go index 122a2f3c..cf3c1f37 100644 --- a/structs/cache/cacher/randomreplacementcache.go +++ b/structs/cache/cacher/randomreplacementcache.go @@ -1,6 +1,7 @@ package cacher import ( + "fmt" "math/rand" "sync" @@ -8,9 +9,6 @@ import ( "github.com/untangle/golang-shared/util" ) -// Used to signal that an index should be ignored in the list of keys -const badKeySignifier string = "badKey" - // Simple cache that removes elements randomly when the cache capacity is met. // O(1) lookups and insertions. For O(1) insertions, space complexity had to be increased. // The size of the cache will grow for every cache deletion since the keys slice can't @@ -21,8 +19,8 @@ type RandomReplacementCache struct { elements map[string]interface{} cacheName string cacheMutex sync.RWMutex - keys []string - keyToIndex map[string]int + + keys []string } func NewRandomReplacementCache(capacity uint, cacheName string) *RandomReplacementCache { @@ -35,8 +33,7 @@ func NewRandomReplacementCache(capacity uint, cacheName string) *RandomReplaceme // keyToIndex map. For a performance bump, just set removed element's keys to // nil when removed. Since keys capacity will exceed that of the maps, give it // a much larger size to avoid too many copies - keys: make([]string, 2*capacity), - keyToIndex: make(map[string]int, capacity), + keys: make([]string, 2*capacity), } } @@ -47,7 +44,8 @@ func (cache *RandomReplacementCache) ForEach(cleanupFunc func(string, interface{ for key, val := range cache.elements { // Remove element if the cleanUp func returns true if cleanupFunc(key, val) { - cache.removeElement(key) + //cache.removeElement(key) + fmt.Print("whateverI ") } } @@ -97,52 +95,48 @@ func (cache *RandomReplacementCache) Put(key string, value interface{}) { } else { // Remove element randomly if the capacity has been met if uint(len(cache.elements)) >= cache.maxCapacity { - cache.removeElement(cache.getRandomKeyForRemoval()) + indexToSwap := rand.Intn(len(cache.keys)) + keyToRemove := cache.keys[indexToSwap] + + // Swap the last key with a random key. Delete the new last key. + lastElementCopy := cache.keys[len(cache.keys)-1] + cache.keys[len(cache.keys)-1] = cache.keys[indexToSwap] + cache.keys[indexToSwap] = lastElementCopy + cache.keys = cache.keys[:len(cache.keys)-1] + + delete(cache.elements, keyToRemove) + logger.Debug("Removed element with key %s from the cache named %s", key, cache.cacheName) } // Add new element cache.elements[key] = value - keyIndex := len(cache.elements) cache.keys = append(cache.keys, key) - cache.keyToIndex[key] = keyIndex logger.Debug("Added element with key %s to the cache named %s", key, cache.cacheName) } } -// This should never be called on an empty cache -func (cache *RandomReplacementCache) getRandomKeyForRemoval() string { - keyForRemoval := badKeySignifier - - for keyForRemoval == badKeySignifier { - keyForRemoval = cache.keys[rand.Intn(len(cache.keys))] - } - - return keyForRemoval -} - -// Don't just use the public function to remove elements since Put/Remove both -// need the write lock. Put calling Remove would result in a deadlock, so -// use this function that they can both call after acquiring the cache's mutex -func (cache *RandomReplacementCache) removeElement(key string) { - if indexToRemove, ok := cache.keyToIndex[key]; ok { - // Deleting keys from the keys list would alter what they're mapped to in keyToIndex - // Instead, set them to a nonsense value to signify they've been removed - cache.keys[indexToRemove] = badKeySignifier +// Remove is an O(n) operation since the key to be removed must be found first +func (cache *RandomReplacementCache) Remove(key string) { + cache.cacheMutex.Lock() + defer cache.cacheMutex.Unlock() - delete(cache.keyToIndex, key) + if _, ok := cache.elements[key]; ok { delete(cache.elements, key) - logger.Debug("Removed element with key %s from the cache named %s", key, cache.cacheName) - } -} + for i := 0; i < len(cache.keys); i++ { + if cache.keys[i] == key { + // Order doesn't matter for the keys slice, so delete the fast way. + // Which is just swapping the element to delete with the last element + // then ignoring the last element of the slice + cache.keys[i] = cache.keys[len(cache.keys)-1] + cache.keys = cache.keys[:len(cache.keys)-1] + } + } -func (cache *RandomReplacementCache) Remove(key string) { - cache.cacheMutex.Lock() - defer cache.cacheMutex.Unlock() - cache.removeElement(key) + } // else the key didn't exists in the cache and nothing should be done } @@ -150,7 +144,6 @@ func (cache *RandomReplacementCache) Clear() { cache.cacheMutex.Lock() defer cache.cacheMutex.Unlock() cache.elements = make(map[string]interface{}, cache.maxCapacity) - cache.keyToIndex = make(map[string]int, cache.maxCapacity) cache.keys = make([]string, cache.maxCapacity) logger.Debug("Cleared cache of name %s", cache.cacheName) } diff --git a/structs/cache/cacher/randomreplacementcache_test.go b/structs/cache/cacher/randomreplacementcache_test.go index 2b4c0ccc..1b0822d5 100644 --- a/structs/cache/cacher/randomreplacementcache_test.go +++ b/structs/cache/cacher/randomreplacementcache_test.go @@ -86,7 +86,6 @@ func (suite *RRCacheTestSuite) TestCapacityExceeded() { // Check if the size is the same, and that the list has changed suite.Equal(int(suite.capacity), len(suite.cache.elements)) - suite.Equal(suite.cache.maxCapacity, uint(len(suite.cache.keyToIndex))) // The keys slice grew since its elements can't be removed suite.True(len(suite.cache.keys) > int(suite.cache.maxCapacity)) @@ -108,5 +107,4 @@ func (suite *RRCacheTestSuite) TestRemove() { _, ok = suite.cache.Get(keyToRemove) suite.False(ok, "The key -- %s -- remained in the cache after being removed", keyToRemove) suite.Equal(suite.cache.maxCapacity-1, uint(len(suite.cache.elements))) - suite.Equal(suite.cache.maxCapacity-1, uint(len(suite.cache.keyToIndex))) } From 63fa4d9f297c4e85ec5aa6534dbb62950707e97c Mon Sep 17 00:00:00 2001 From: Adam Briles Date: Tue, 30 Aug 2022 12:17:29 -0600 Subject: [PATCH 20/28] finished refractoring rrcache, removing sweptcache and sweeper interface --- .../cache/cacher/randomreplacementcache.go | 114 +++++++++++------- .../cacher/randomreplacementcache_test.go | 15 +-- util/util.go | 13 -- 3 files changed, 79 insertions(+), 63 deletions(-) diff --git a/structs/cache/cacher/randomreplacementcache.go b/structs/cache/cacher/randomreplacementcache.go index cf3c1f37..dc369c58 100644 --- a/structs/cache/cacher/randomreplacementcache.go +++ b/structs/cache/cacher/randomreplacementcache.go @@ -1,14 +1,17 @@ package cacher import ( - "fmt" "math/rand" "sync" "github.com/untangle/golang-shared/services/logger" - "github.com/untangle/golang-shared/util" ) +type Value struct { + keyIndex uint + value interface{} +} + // Simple cache that removes elements randomly when the cache capacity is met. // O(1) lookups and insertions. For O(1) insertions, space complexity had to be increased. // The size of the cache will grow for every cache deletion since the keys slice can't @@ -16,24 +19,21 @@ import ( // The cache can be read from by multiple threads, but written to by one. type RandomReplacementCache struct { maxCapacity uint - elements map[string]interface{} + elements map[string]*Value cacheName string cacheMutex sync.RWMutex - keys []string + keys []string + totalElements uint } func NewRandomReplacementCache(capacity uint, cacheName string) *RandomReplacementCache { return &RandomReplacementCache{ - maxCapacity: capacity, - elements: make(map[string]interface{}, capacity), - cacheName: cacheName, - - // Removing elements from the keys slice would cause an entire update of the - // keyToIndex map. For a performance bump, just set removed element's keys to - // nil when removed. Since keys capacity will exceed that of the maps, give it - // a much larger size to avoid too many copies - keys: make([]string, 2*capacity), + maxCapacity: capacity, + elements: make(map[string]*Value, capacity), + cacheName: cacheName, + keys: make([]string, capacity), + totalElements: 0, } } @@ -44,19 +44,33 @@ func (cache *RandomReplacementCache) ForEach(cleanupFunc func(string, interface{ for key, val := range cache.elements { // Remove element if the cleanUp func returns true if cleanupFunc(key, val) { - //cache.removeElement(key) - fmt.Print("whateverI ") + cache.removeWithoutLock(key) + } } } +// It's useful to get the keys directly from the map instead of the array of keys +// Since the array of keys will have empty strings in it +func getMapKeys(m *map[string]*Value) []string { + keys := make([]string, len(*m)) + + i := 0 + for key := range *m { + keys[i] = key + i++ + } + + return keys +} + func (cache *RandomReplacementCache) GetIterator() func() (string, interface{}, bool) { // Once an iterator has been retrieved, it captures the state of // of the cache. If the cache is updated the iterator won't contain // the update cache.cacheMutex.RLock() - keys := util.GetMapKeys(cache.elements) + keys := getMapKeys(&cache.elements) cache.cacheMutex.RUnlock() i := 0 @@ -79,9 +93,11 @@ func (cache *RandomReplacementCache) Get(key string) (interface{}, bool) { cache.cacheMutex.RLock() defer cache.cacheMutex.RUnlock() - value, ok := cache.elements[key] - - return value, ok + if value, ok := cache.elements[key]; ok { + return value.value, ok + } else { + return nil, ok + } } func (cache *RandomReplacementCache) Put(key string, value interface{}) { @@ -90,19 +106,27 @@ func (cache *RandomReplacementCache) Put(key string, value interface{}) { // Update element if already present in cache if _, ok := cache.elements[key]; ok { - cache.elements[key] = value + cache.elements[key].value = value logger.Debug("Updated the element with key %s in the cache named %s", key, cache.cacheName) } else { // Remove element randomly if the capacity has been met - if uint(len(cache.elements)) >= cache.maxCapacity { + if cache.totalElements >= cache.maxCapacity { indexToSwap := rand.Intn(len(cache.keys)) keyToRemove := cache.keys[indexToSwap] // Swap the last key with a random key. Delete the new last key. lastElementCopy := cache.keys[len(cache.keys)-1] + + // Update index of the value getting swapped + cache.elements[lastElementCopy].keyIndex = uint(indexToSwap) + cache.keys[len(cache.keys)-1] = cache.keys[indexToSwap] cache.keys[indexToSwap] = lastElementCopy - cache.keys = cache.keys[:len(cache.keys)-1] + + // Don't just reassign the keys list, it'll alter the size and + // throw everything off + cache.keys[len(cache.keys)-1] = "" + cache.totalElements -= 1 delete(cache.elements, keyToRemove) @@ -110,46 +134,50 @@ func (cache *RandomReplacementCache) Put(key string, value interface{}) { } // Add new element - cache.elements[key] = value - cache.keys = append(cache.keys, key) - + cache.totalElements += 1 + cache.elements[key] = &Value{keyIndex: cache.totalElements - 1, value: value} + cache.keys[cache.totalElements-1] = key logger.Debug("Added element with key %s to the cache named %s", key, cache.cacheName) - } } -// Remove is an O(n) operation since the key to be removed must be found first -func (cache *RandomReplacementCache) Remove(key string) { - cache.cacheMutex.Lock() - defer cache.cacheMutex.Unlock() - +func (cache *RandomReplacementCache) removeWithoutLock(key string) { if _, ok := cache.elements[key]; ok { - delete(cache.elements, key) + indexToRemove := cache.elements[key].keyIndex - for i := 0; i < len(cache.keys); i++ { - if cache.keys[i] == key { - // Order doesn't matter for the keys slice, so delete the fast way. - // Which is just swapping the element to delete with the last element - // then ignoring the last element of the slice - cache.keys[i] = cache.keys[len(cache.keys)-1] - cache.keys = cache.keys[:len(cache.keys)-1] - } - } + // Order doesn't matter for the keys slice, so delete the fast way. + // Which is just swapping the element to delete with the last element + // then ignoring the last element of the slice + cache.keys[indexToRemove] = cache.keys[len(cache.keys)-1] + cache.keys = cache.keys[:len(cache.keys)-1] + // Update index of moved element + cache.elements[key].keyIndex = indexToRemove + + delete(cache.elements, key) + cache.totalElements -= 1 } // else the key didn't exists in the cache and nothing should be done } +// Remove is an O(n) operation since the key to be removed must be found first +func (cache *RandomReplacementCache) Remove(key string) { + cache.cacheMutex.Lock() + defer cache.cacheMutex.Unlock() + cache.removeWithoutLock(key) +} + func (cache *RandomReplacementCache) Clear() { cache.cacheMutex.Lock() defer cache.cacheMutex.Unlock() - cache.elements = make(map[string]interface{}, cache.maxCapacity) + cache.elements = make(map[string]*Value, cache.maxCapacity) cache.keys = make([]string, cache.maxCapacity) + cache.totalElements = 0 logger.Debug("Cleared cache of name %s", cache.cacheName) } func (cache *RandomReplacementCache) GetCurrentCapacity() int { cache.cacheMutex.RLock() defer cache.cacheMutex.RUnlock() - return len(cache.elements) + return int(cache.totalElements) } diff --git a/structs/cache/cacher/randomreplacementcache_test.go b/structs/cache/cacher/randomreplacementcache_test.go index 1b0822d5..2b21b8fa 100644 --- a/structs/cache/cacher/randomreplacementcache_test.go +++ b/structs/cache/cacher/randomreplacementcache_test.go @@ -5,7 +5,6 @@ import ( "testing" "github.com/stretchr/testify/suite" - "github.com/untangle/golang-shared/util" ) type RRCacheTestSuite struct { @@ -79,16 +78,18 @@ func (suite *RRCacheTestSuite) TestClear() { } func (suite *RRCacheTestSuite) TestCapacityExceeded() { - keysBeforePut := util.GetMapKeys(suite.cache.elements) + keysBeforePut := getMapKeys(&suite.cache.elements) + newKey := "6" + newVal := 6 suite.cache.Put("6", 6) - keysAfterPut := util.GetMapKeys(suite.cache.elements) + keysAfterPut := getMapKeys(&suite.cache.elements) - // Check if the size is the same, and that the list has changed suite.Equal(int(suite.capacity), len(suite.cache.elements)) - // The keys slice grew since its elements can't be removed - suite.True(len(suite.cache.keys) > int(suite.cache.maxCapacity)) + val, ok := suite.cache.Get("6") + suite.True(ok, "The cache did not contain the newly added value with key %s", newKey) + suite.Equal(newVal, val, "The key %s did not have the expected value of %d", newKey, newVal) suite.NotEqual(keysAfterPut, keysBeforePut) } @@ -106,5 +107,5 @@ func (suite *RRCacheTestSuite) TestRemove() { _, ok = suite.cache.Get(keyToRemove) suite.False(ok, "The key -- %s -- remained in the cache after being removed", keyToRemove) - suite.Equal(suite.cache.maxCapacity-1, uint(len(suite.cache.elements))) + suite.Equal(suite.cache.maxCapacity-1, suite.cache.totalElements) } diff --git a/util/util.go b/util/util.go index daa38e6c..7d575a28 100644 --- a/util/util.go +++ b/util/util.go @@ -9,16 +9,3 @@ func ContainsString(s []string, e string) bool { } return false } - -// Returns a slice of all keys in a map -func GetMapKeys(m map[string]interface{}) []string { - keys := make([]string, len(m)) - - i := 0 - for key := range m { - keys[i] = key - i++ - } - - return keys -} From f3f665bd3eb016afb064e3b66f6696b35711d771 Mon Sep 17 00:00:00 2001 From: Adam Briles Date: Tue, 30 Aug 2022 15:11:09 -0600 Subject: [PATCH 21/28] Caches and sweeper in final state minus some cleanup --- structs/cache/cacher/cacher.go | 2 +- structs/cache/cacher/lrucache.go | 78 ++++++++++--------- structs/cache/cacher/lrucache_test.go | 22 +++--- .../cache/cacher/randomreplacementcache.go | 68 +++++++--------- .../cacher/randomreplacementcache_test.go | 69 +++++++++++++--- structs/cache/sweeper/sweeper.go | 9 --- structs/cache/sweeper/sweepontime.go | 47 ----------- structs/cache/sweeper/sweepontime_test.go | 61 --------------- structs/cache/sweptcache.go | 49 ------------ structs/cache/sweptcache_test.go | 77 ------------------ 10 files changed, 137 insertions(+), 345 deletions(-) delete mode 100644 structs/cache/sweeper/sweeper.go delete mode 100644 structs/cache/sweeper/sweepontime.go delete mode 100644 structs/cache/sweeper/sweepontime_test.go delete mode 100644 structs/cache/sweptcache.go delete mode 100644 structs/cache/sweptcache_test.go diff --git a/structs/cache/cacher/cacher.go b/structs/cache/cacher/cacher.go index c82f6b8a..ba740211 100644 --- a/structs/cache/cacher/cacher.go +++ b/structs/cache/cacher/cacher.go @@ -8,7 +8,7 @@ type Cacher interface { Remove(string) // An iterator - GetIterator() func() (string, interface{}, bool) + //GetIterator() func() (string, interface{}, bool) // Runs a given function on each cache element ForEach(func(string, interface{}) bool) diff --git a/structs/cache/cacher/lrucache.go b/structs/cache/cacher/lrucache.go index 8ead7327..81ad43cb 100644 --- a/structs/cache/cacher/lrucache.go +++ b/structs/cache/cacher/lrucache.go @@ -15,6 +15,7 @@ type KeyPair struct { // A simple LRU Cache implementation. The least recently used elements // in the cache are removed if the cache's max capacity is hit. The cache's // mutex cannot be a RWMutex since Gets alter the cache's underlying data structures. +// Slower concurrent performance than a Random Removal cache, but more cache hits. // O(1) reads and O(1) insertions. type LruCache struct { capacity uint @@ -41,8 +42,8 @@ func (cache *LruCache) ForEach(cleanupFunc func(string, interface{}) bool) { for key, val := range cache.elements { // Remove element if the cleanUp func returns true - if cleanupFunc(key, val) { - cache.removeElement(key) + if cleanupFunc(key, val.Value.(*list.Element).Value.(KeyPair).Value) { + cache.removeElementNoLock(key) } } @@ -66,39 +67,39 @@ func (cache *LruCache) Get(key string) (interface{}, bool) { return value, found } -func (cache *LruCache) GetIterator() func() (string, *interface{}, bool) { - // Once an iterator has been retrieved, it captures the state of - // of the cache. If the cache is updated the iterator won't contain - // the update - cache.cacheMutex.Lock() - listSnapshot := copyLinkedList(cache.list) - cache.cacheMutex.Unlock() - - node := listSnapshot.Front() - // Return key, val, and if there is anything left to iterate over - return func() (string, *interface{}, bool) { - if node != nil { - - currentNode := node - currentKey := currentNode.Value.(*list.Element).Value.(KeyPair).Key - currentValue := currentNode.Value.(*list.Element).Value.(KeyPair).Value - - node = node.Next() - - return currentKey, ¤tValue, true - } else { - return "", nil, false - } - } -} - -func copyLinkedList(original *list.List) *list.List { - listCopy := list.New() - for node := original.Front(); node != nil; node = node.Next() { - listCopy.PushBack(node.Value) - } - return listCopy -} +// func (cache *LruCache) GetIterator() func() (string, interface{}, bool) { +// // Once an iterator has been retrieved, it captures the state of +// // of the cache. If the cache is updated the iterator won't contain +// // the update +// cache.cacheMutex.Lock() +// listSnapshot := copyLinkedList(cache.list) +// cache.cacheMutex.Unlock() + +// node := listSnapshot.Front() +// // Return key, val, and if there is anything left to iterate over +// return func() (string, interface{}, bool) { +// if node != nil { + +// currentNode := node +// currentKey := currentNode.Value.(*list.Element).Value.(KeyPair).Key +// currentValue := currentNode.Value.(*list.Element).Value.(KeyPair).Value + +// node = node.Next() + +// return currentKey, currentValue, true +// } else { +// return "", nil, false +// } +// } +// } + +// func copyLinkedList(original *list.List) *list.List { +// listCopy := list.New() +// for node := original.Front(); node != nil; node = node.Next() { +// listCopy.PushBack(node.Value) +// } +// return listCopy +// } // Add an item to the cache and move it to the front of the queue. // If the item's key is already in the cache, update the key's value @@ -137,8 +138,9 @@ func (cache *LruCache) Put(key string, value interface{}) { } } -// Does NOT take the cache's lock. Functions calling removeElement() need to -func (cache *LruCache) removeElement(key string) { +// Does NOT take the cache's lock. Functions calling removeElementNoLock() +// need to do it themselves +func (cache *LruCache) removeElementNoLock(key string) { if node, ok := cache.elements[key]; ok { delete(cache.elements, key) cache.list.Remove(node) @@ -150,7 +152,7 @@ func (cache *LruCache) removeElement(key string) { func (cache *LruCache) Remove(key string) { cache.cacheMutex.Lock() defer cache.cacheMutex.Unlock() - cache.removeElement(key) + cache.removeElementNoLock(key) } // Clear all all internal data structures diff --git a/structs/cache/cacher/lrucache_test.go b/structs/cache/cacher/lrucache_test.go index 38af8e00..ed0c2bbc 100644 --- a/structs/cache/cacher/lrucache_test.go +++ b/structs/cache/cacher/lrucache_test.go @@ -28,17 +28,17 @@ func TestLruCacheTestSuite(t *testing.T) { suite.Run(t, new(LruCacheTestSuite)) } -func (suite *LruCacheTestSuite) TestGetIterator() { - next := suite.cache.GetIterator() - - count := 0 - for key, _, isNext := next(); isNext; key, _, isNext = next() { - _, ok := suite.cache.Get(key) - suite.True(ok, "The iterator returned a value not in the cache") - count += 1 - } - suite.Equal(suite.cache.capacity, uint(count)) -} +// func (suite *LruCacheTestSuite) TestGetIterator() { +// next := suite.cache.GetIterator() + +// count := 0 +// for key, _, isNext := next(); isNext; key, _, isNext = next() { +// _, ok := suite.cache.Get(key) +// suite.True(ok, "The iterator returned a value not in the cache") +// count += 1 +// } +// suite.Equal(suite.cache.capacity, uint(count)) +// } func (suite *LruCacheTestSuite) TestGetMostRecentlyUsed() { expectedKey, expectedValue := "4", 4 diff --git a/structs/cache/cacher/randomreplacementcache.go b/structs/cache/cacher/randomreplacementcache.go index dc369c58..10944204 100644 --- a/structs/cache/cacher/randomreplacementcache.go +++ b/structs/cache/cacher/randomreplacementcache.go @@ -43,9 +43,8 @@ func (cache *RandomReplacementCache) ForEach(cleanupFunc func(string, interface{ for key, val := range cache.elements { // Remove element if the cleanUp func returns true - if cleanupFunc(key, val) { + if cleanupFunc(key, val.value) { cache.removeWithoutLock(key) - } } @@ -65,29 +64,29 @@ func getMapKeys(m *map[string]*Value) []string { return keys } -func (cache *RandomReplacementCache) GetIterator() func() (string, interface{}, bool) { - // Once an iterator has been retrieved, it captures the state of - // of the cache. If the cache is updated the iterator won't contain - // the update - cache.cacheMutex.RLock() - keys := getMapKeys(&cache.elements) - cache.cacheMutex.RUnlock() - - i := 0 - // Return key, val, and if there is anything left to iterate over - return func() (string, interface{}, bool) { - if i == len(keys) { - return "", nil, false - } - - currentKey := keys[i] - - // The value could be nil if the map was altered - value, _ := cache.Get(currentKey) - i += 1 - return currentKey, &value, true - } -} +// func (cache *RandomReplacementCache) GetIterator() func() (string, interface{}, bool) { +// // Once an iterator has been retrieved, it captures the state of +// // of the cache. If the cache is updated the iterator won't contain +// // the update +// cache.cacheMutex.RLock() +// keys := getMapKeys(&cache.elements) +// cache.cacheMutex.RUnlock() + +// i := 0 +// // Return key, val, and if there is anything left to iterate over +// return func() (string, interface{}, bool) { +// if i == len(keys) { +// return "", nil, false +// } + +// currentKey := keys[i] + +// // The value could be nil if the map was altered +// value, _ := cache.Get(currentKey) +// i += 1 +// return currentKey, &value, true +// } +// } func (cache *RandomReplacementCache) Get(key string) (interface{}, bool) { cache.cacheMutex.RLock() @@ -113,22 +112,7 @@ func (cache *RandomReplacementCache) Put(key string, value interface{}) { if cache.totalElements >= cache.maxCapacity { indexToSwap := rand.Intn(len(cache.keys)) keyToRemove := cache.keys[indexToSwap] - - // Swap the last key with a random key. Delete the new last key. - lastElementCopy := cache.keys[len(cache.keys)-1] - - // Update index of the value getting swapped - cache.elements[lastElementCopy].keyIndex = uint(indexToSwap) - - cache.keys[len(cache.keys)-1] = cache.keys[indexToSwap] - cache.keys[indexToSwap] = lastElementCopy - - // Don't just reassign the keys list, it'll alter the size and - // throw everything off - cache.keys[len(cache.keys)-1] = "" - cache.totalElements -= 1 - - delete(cache.elements, keyToRemove) + cache.removeWithoutLock(keyToRemove) logger.Debug("Removed element with key %s from the cache named %s", key, cache.cacheName) } @@ -149,7 +133,7 @@ func (cache *RandomReplacementCache) removeWithoutLock(key string) { // Which is just swapping the element to delete with the last element // then ignoring the last element of the slice cache.keys[indexToRemove] = cache.keys[len(cache.keys)-1] - cache.keys = cache.keys[:len(cache.keys)-1] + cache.keys[len(cache.keys)-1] = "" // Update index of moved element cache.elements[key].keyIndex = indexToRemove diff --git a/structs/cache/cacher/randomreplacementcache_test.go b/structs/cache/cacher/randomreplacementcache_test.go index 2b21b8fa..776e6103 100644 --- a/structs/cache/cacher/randomreplacementcache_test.go +++ b/structs/cache/cacher/randomreplacementcache_test.go @@ -4,6 +4,7 @@ import ( "strconv" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" ) @@ -29,23 +30,71 @@ func (suite *RRCacheTestSuite) SetupTest() { } } -func (suite *RRCacheTestSuite) TestNextElement() { - next := suite.cache.GetIterator() - var key string - var isNext bool +// The suite is not being used since +// a pointer is required as a cache value +// if any alteration to the value are going to +// be successful +func TestForEachElementMutation(t *testing.T) { + capacity := 5 + cacheName := "cacheMutationTest" + testCache := *NewRandomReplacementCache(uint(capacity), cacheName) + + for i := 0; i < capacity; i++ { + // Create a copy of i so all elements don't + // point to the same int + newVal := i + testCache.Put(strconv.Itoa(int(i)), &newVal) + } + + assert.Equal(t, testCache.GetCurrentCapacity(), capacity) + + mutateElement := func(s string, i interface{}) bool { + deleteElement := false + if *(i).(*int) != 3 { + *(i).(*int) = 3 + } - count := 0 - for key, _, isNext = next(); isNext == true; key, _, isNext = next() { - _, ok := suite.cache.elements[key] + return deleteElement + } + testCache.ForEach(mutateElement) - suite.True(ok, "The iterator retrieved a value not in the cache") - count += 1 + for key, val := range testCache.elements { + assert.Equal(t, 3, *(val.value.(*int)), "The key %s was not altered as expected", key) } +} + +func (suite *RRCacheTestSuite) TestForEachElementDeletion() { + suite.cache.ForEach(func(s string, i interface{}) bool { + deleteElement := false + if i.(int) < 4 { + deleteElement = true + } - suite.Equal(suite.capacity, uint(count), "The iterator did not iterate over ever element in the cache") + return deleteElement + }) + for key, val := range suite.cache.elements { + suite.Equal(4, val.value.(int), "The key %s was not altered as expected", key) + } } +// func (suite *RRCacheTestSuite) TestNextElement() { +// next := suite.cache.GetIterator() +// var key string +// var isNext bool + +// count := 0 +// for key, _, isNext = next(); isNext == true; key, _, isNext = next() { +// _, ok := suite.cache.elements[key] + +// suite.True(ok, "The iterator retrieved a value not in the cache") +// count += 1 +// } + +// suite.Equal(suite.capacity, uint(count), "The iterator did not iterate over ever element in the cache") + +// } + func (suite *RRCacheTestSuite) TestGet() { for i := 0; i < int(suite.capacity); i++ { value, ok := suite.cache.Get(strconv.Itoa(i)) diff --git a/structs/cache/sweeper/sweeper.go b/structs/cache/sweeper/sweeper.go deleted file mode 100644 index 34325188..00000000 --- a/structs/cache/sweeper/sweeper.go +++ /dev/null @@ -1,9 +0,0 @@ -package sweeper - -// Interface for altering/removing elements from a cache -// StartSweep should kickoff a goroutine that scans the a -// cache on a set interval. -type Sweeper interface { - StartSweeping(func()) - StopSweeping() -} diff --git a/structs/cache/sweeper/sweepontime.go b/structs/cache/sweeper/sweepontime.go deleted file mode 100644 index dfb2e2fe..00000000 --- a/structs/cache/sweeper/sweepontime.go +++ /dev/null @@ -1,47 +0,0 @@ -package sweeper - -import ( - "time" - - "github.com/untangle/golang-shared/services/logger" -) - -type SweepOnTime struct { - shutdownChannel chan bool - waitTime time.Duration -} - -func NewSweepOnTime(waitTime time.Duration) *SweepOnTime { - return &SweepOnTime{ - shutdownChannel: make(chan bool), - waitTime: waitTime, - } -} - -func (sweeper *SweepOnTime) StartSweeping(cleanupFunc func()) { - go sweeper.runCleanup(cleanupFunc) -} - -func (sweeper *SweepOnTime) StopSweeping() { - sweeper.shutdownChannel <- true - - select { - case <-sweeper.shutdownChannel: - logger.Info("Successful shutdown of clean up \n") - case <-time.After(10 * time.Second): - logger.Warn("Failed to properly shutdown cleanupTask\n") - } -} - -func (sweeper *SweepOnTime) runCleanup(cleanupFunc func()) { - for { - select { - case <-sweeper.shutdownChannel: - sweeper.shutdownChannel <- true - return - case <-time.After(sweeper.waitTime): - - cleanupFunc() - } - } -} diff --git a/structs/cache/sweeper/sweepontime_test.go b/structs/cache/sweeper/sweepontime_test.go deleted file mode 100644 index e6a71d51..00000000 --- a/structs/cache/sweeper/sweepontime_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package sweeper - -import ( - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/suite" -) - -type SweepOnTimeTestSuite struct { - suite.Suite - sweeper SweepOnTime - sweepInterval time.Duration - - // Cache used to test if the cleanup task gets run - cache map[string]int - - // A value used as sweep criteria - cacheMaxValue int -} - -func (suite *SweepOnTimeTestSuite) getCleanupFunc() func() { - cleanupCache := &(suite.cache) - elementMaxValue := suite.cacheMaxValue - - // Simple function to remove a cache element if it's over a value - return func() { - for key, val := range *cleanupCache { - if val < elementMaxValue { - delete(*cleanupCache, key) - } - } - } -} - -func TestSweepOnTimeTestSuite(t *testing.T) { - suite.Run(t, new(SweepOnTimeTestSuite)) -} - -func (suite *SweepOnTimeTestSuite) SetupTest() { - suite.sweepInterval = 5 * time.Millisecond - suite.cache = map[string]int{"1": 1, "2": 2, "3": 3, "4": 4} - suite.cacheMaxValue = 3 - suite.sweeper = *NewSweepOnTime(suite.sweepInterval) - - suite.sweeper.StartSweeping(suite.getCleanupFunc()) -} - -func (suite *SweepOnTimeTestSuite) TestSwept() { - time.Sleep((suite.sweepInterval) + 1) - - for key, val := range suite.cache { - fmt.Println(key) - suite.True(val >= suite.cacheMaxValue, "The test cache was not swept") - } -} - -func (suite *SweepOnTimeTestSuite) TearDownTest() { - suite.sweeper.StopSweeping() -} diff --git a/structs/cache/sweptcache.go b/structs/cache/sweptcache.go deleted file mode 100644 index 5851a154..00000000 --- a/structs/cache/sweptcache.go +++ /dev/null @@ -1,49 +0,0 @@ -package cache - -import ( - "github.com/untangle/golang-shared/structs/cache/cacher" - "github.com/untangle/golang-shared/structs/cache/sweeper" -) - -// Cache with a struct responsible for kicking off sweeps of the cache. -type SweptCache struct { - cacher.Cacher - - sweeper sweeper.Sweeper -} - -// The sweeper runs a function when certain criteria is met. To give the function being run by the sweeper -// access to the cache, use a closure. -func (sweptCache *SweptCache) generateCleanupTask(cleanupFunc func(string, interface{}) bool) func() { - - cache := sweptCache.Cacher - - return func() { - - getNext := cache.GetIterator() - - for key, value, ok := getNext(); ok; key, value, ok = getNext() { - if cleanupFunc(key, value) { - sweptCache.Cacher.Remove(key) - } - } - - } -} - -func NewSweptCache(cache cacher.Cacher, sweeper sweeper.Sweeper) *SweptCache { - - return &SweptCache{cache, sweeper} -} - -// Starts sweeping the cache. The function provided will be run on every element in the cache -// once a sweep is triggered. -// The key of the cache element, and a pointer to it, must be handled by the provided function. -// If false is returned from the provided function, the cache element will be removed. -func (sweptCache *SweptCache) StartSweeping(cleanupFunc func(string, interface{}) bool) { - sweptCache.sweeper.StartSweeping(sweptCache.generateCleanupTask(cleanupFunc)) -} - -func (sweptCache *SweptCache) StopSweeping() { - sweptCache.sweeper.StopSweeping() -} diff --git a/structs/cache/sweptcache_test.go b/structs/cache/sweptcache_test.go deleted file mode 100644 index 8ac015d8..00000000 --- a/structs/cache/sweptcache_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package cache - -import ( - "strconv" - "testing" - "time" - - "github.com/stretchr/testify/suite" - "github.com/untangle/golang-shared/structs/cache/cacher" - "github.com/untangle/golang-shared/structs/cache/sweeper" -) - -type TestSweptCache struct { - suite.Suite - sweptCache SweptCache - - sweepInterval int -} - -func TestSweptCacheSuite(t *testing.T) { - suite.Run(t, &TestSweptCache{}) -} - -func (suite *TestSweptCache) SetupTest() { - suite.sweepInterval = 1 - suite.sweptCache = *NewSweptCache(cacher.NewRandomReplacementCache(5, "sweptCacheTest"), sweeper.NewSweepOnTime(time.Duration(suite.sweepInterval)*time.Second)) - - for i := 0; i < 5; i++ { - newVal := i - suite.sweptCache.Put(strconv.Itoa(int(i)), &newVal) - } -} - -func (suite *TestSweptCache) TestElementDeletionFunction() { - - // Remove elements with a value less than 3. Run every second - suite.sweptCache.StartSweeping(func(s string, i interface{}) bool { - deleteElement := false - if *(*(i.(*interface{}))).(*int) < 3 { - deleteElement = true - } - - return deleteElement - }) - - time.Sleep((time.Duration(suite.sweepInterval) + 1) * time.Second) - - next := suite.sweptCache.GetIterator() - - for key, val, ok := next(); ok; key, val, ok = next() { - suite.True(*(*(val.(*interface{}))).(*int) >= 3, "The key %s was not swept as expected", key) - } - -} - -func (suite *TestSweptCache) TestElementMutationFunction() { - suite.sweptCache.StartSweeping(func(s string, i interface{}) bool { - if *(*(i.(*interface{}))).(*int) < 4 { - *(*(i.(*interface{}))).(*int) = 4 - } - - return false - }) - - time.Sleep((time.Duration(suite.sweepInterval) + 2) * time.Second) - - next := suite.sweptCache.GetIterator() - for key, val, ok := next(); ok; key, val, ok = next() { - - suite.True(*(*(val.(*interface{}))).(*int) == 4, "The key %s was not altered as expected", key) - } - -} - -func (suite *TestSweptCache) TearDownTest() { - suite.sweptCache.StopSweeping() -} From 8c79a59b2eead5f1b55c02e9231a767f1cc06824 Mon Sep 17 00:00:00 2001 From: Adam Briles Date: Tue, 30 Aug 2022 15:11:28 -0600 Subject: [PATCH 22/28] Caches and sweeper in final state minus some cleanup --- structs/cache/timesweptcache.go | 50 +++++++++++++++ structs/cache/timesweptcache_test.go | 94 ++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 structs/cache/timesweptcache.go create mode 100644 structs/cache/timesweptcache_test.go diff --git a/structs/cache/timesweptcache.go b/structs/cache/timesweptcache.go new file mode 100644 index 00000000..2dcf36d3 --- /dev/null +++ b/structs/cache/timesweptcache.go @@ -0,0 +1,50 @@ +package cache + +import ( + "time" + + "github.com/untangle/golang-shared/services/logger" + "github.com/untangle/golang-shared/structs/cache/cacher" +) + +type TimeSweptCache struct { + cacher.Cacher + + shutdownChannel chan bool + waitTime time.Duration +} + +func NewTimeSweptCache(cache cacher.Cacher, waitTime time.Duration) *TimeSweptCache { + return &TimeSweptCache{ + Cacher: cache, + shutdownChannel: make(chan bool), + waitTime: waitTime, + } +} + +func (sweeper *TimeSweptCache) StartSweeping(cleanupFunc func(string, interface{}) bool) { + go sweeper.runCleanup(cleanupFunc) +} + +func (sweeper *TimeSweptCache) runCleanup(cleanupFunc func(string, interface{}) bool) { + for { + select { + case <-sweeper.shutdownChannel: + sweeper.shutdownChannel <- true + return + case <-time.After(sweeper.waitTime): + sweeper.Cacher.ForEach(cleanupFunc) + } + } +} + +func (sweeper *TimeSweptCache) StopSweeping() { + sweeper.shutdownChannel <- true + + select { + case <-sweeper.shutdownChannel: + logger.Info("Successful shutdown of clean up \n") + case <-time.After(10 * time.Second): + logger.Warn("Failed to properly shutdown cleanupTask\n") + } +} diff --git a/structs/cache/timesweptcache_test.go b/structs/cache/timesweptcache_test.go new file mode 100644 index 00000000..9c3e7f8d --- /dev/null +++ b/structs/cache/timesweptcache_test.go @@ -0,0 +1,94 @@ +package cache + +import ( + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/suite" +) + +type MockCache struct { + elements map[string]interface{} +} + +func (cache *MockCache) Get(key string) (interface{}, bool) { + val, ok := cache.elements[key] + return val, ok +} + +func (cache *MockCache) Put(key string, value interface{}) { + cache.elements[key] = value +} + +func (cache *MockCache) Clear() { + cache.elements = make(map[string]interface{}) +} + +func (cache *MockCache) Remove(key string) { + delete(cache.elements, key) +} + +func (cache *MockCache) GetIterator() func() (string, interface{}, bool) { + return func() (string, interface{}, bool) { + return "true", "true", false + } +} + +func (cache *MockCache) ForEach(cleanUp func(string, interface{}) bool) { + for key, val := range cache.elements { + if cleanUp(key, val) { + cache.Remove(key) + } + } +} + +func NewMockCache() *MockCache { + return &MockCache{elements: make(map[string]interface{})} +} + +type TestTimeSweptCache struct { + suite.Suite + timeSweptCache TimeSweptCache + + sweepInterval int +} + +func TestTimeSweptCacheSuite(t *testing.T) { + suite.Run(t, &TestTimeSweptCache{}) +} + +func (suite *TestTimeSweptCache) SetupTest() { + suite.sweepInterval = 1 + suite.timeSweptCache = *NewTimeSweptCache(NewMockCache(), time.Duration(suite.sweepInterval)) + + for i := 0; i < 5; i++ { + suite.timeSweptCache.Put(strconv.Itoa(int(i)), i) + } +} + +func (suite *TestTimeSweptCache) TestCleanupTaskRan() { + + // Remove elements that aren't equal to 4 + suite.timeSweptCache.StartSweeping(func(s string, i interface{}) bool { + deleteElement := false + if i.(int) != 4 { + deleteElement = true + } + + return deleteElement + }) + + time.Sleep((time.Duration(suite.sweepInterval) + 1) * time.Second) + + _, ok := suite.timeSweptCache.Get("1") + suite.False(ok, "The cleanup task was not run as expected") + + _, ok = suite.timeSweptCache.Get("4") + suite.True(ok, "The cleanup task removed an unexpected cache element") + +} + +func (suite *TestTimeSweptCache) TearDownTest() { + suite.timeSweptCache.StopSweeping() +} From c6338653d3776c2df80a3893409d1a7770e1d1e3 Mon Sep 17 00:00:00 2001 From: Adam Briles Date: Wed, 31 Aug 2022 10:23:38 -0600 Subject: [PATCH 23/28] moved cache to utils --- {structs => util}/cache/cacher/cacher.go | 0 {structs => util}/cache/cacher/lrucache.go | 0 {structs => util}/cache/cacher/lrucache_test.go | 0 {structs => util}/cache/cacher/randomreplacementcache.go | 0 {structs => util}/cache/cacher/randomreplacementcache_test.go | 0 {structs => util}/cache/timesweptcache.go | 0 {structs => util}/cache/timesweptcache_test.go | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename {structs => util}/cache/cacher/cacher.go (100%) rename {structs => util}/cache/cacher/lrucache.go (100%) rename {structs => util}/cache/cacher/lrucache_test.go (100%) rename {structs => util}/cache/cacher/randomreplacementcache.go (100%) rename {structs => util}/cache/cacher/randomreplacementcache_test.go (100%) rename {structs => util}/cache/timesweptcache.go (100%) rename {structs => util}/cache/timesweptcache_test.go (100%) diff --git a/structs/cache/cacher/cacher.go b/util/cache/cacher/cacher.go similarity index 100% rename from structs/cache/cacher/cacher.go rename to util/cache/cacher/cacher.go diff --git a/structs/cache/cacher/lrucache.go b/util/cache/cacher/lrucache.go similarity index 100% rename from structs/cache/cacher/lrucache.go rename to util/cache/cacher/lrucache.go diff --git a/structs/cache/cacher/lrucache_test.go b/util/cache/cacher/lrucache_test.go similarity index 100% rename from structs/cache/cacher/lrucache_test.go rename to util/cache/cacher/lrucache_test.go diff --git a/structs/cache/cacher/randomreplacementcache.go b/util/cache/cacher/randomreplacementcache.go similarity index 100% rename from structs/cache/cacher/randomreplacementcache.go rename to util/cache/cacher/randomreplacementcache.go diff --git a/structs/cache/cacher/randomreplacementcache_test.go b/util/cache/cacher/randomreplacementcache_test.go similarity index 100% rename from structs/cache/cacher/randomreplacementcache_test.go rename to util/cache/cacher/randomreplacementcache_test.go diff --git a/structs/cache/timesweptcache.go b/util/cache/timesweptcache.go similarity index 100% rename from structs/cache/timesweptcache.go rename to util/cache/timesweptcache.go diff --git a/structs/cache/timesweptcache_test.go b/util/cache/timesweptcache_test.go similarity index 100% rename from structs/cache/timesweptcache_test.go rename to util/cache/timesweptcache_test.go From 5087434b20d381884b6871c59da19c7a6e883ddd Mon Sep 17 00:00:00 2001 From: Adam Briles Date: Wed, 31 Aug 2022 10:31:46 -0600 Subject: [PATCH 24/28] moved cache to utils --- util/cache/timesweptcache.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/cache/timesweptcache.go b/util/cache/timesweptcache.go index 2dcf36d3..99b39bbe 100644 --- a/util/cache/timesweptcache.go +++ b/util/cache/timesweptcache.go @@ -4,7 +4,7 @@ import ( "time" "github.com/untangle/golang-shared/services/logger" - "github.com/untangle/golang-shared/structs/cache/cacher" + "github.com/untangle/golang-shared/util/cache/cacher" ) type TimeSweptCache struct { From e5755109dbd272ad288574eecd84bd43f5eb00de Mon Sep 17 00:00:00 2001 From: Adam Briles Date: Wed, 31 Aug 2022 11:03:44 -0600 Subject: [PATCH 25/28] aded some prints for debugging --- util/cache/cacher/randomreplacementcache.go | 1 - util/cache/timesweptcache.go | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/util/cache/cacher/randomreplacementcache.go b/util/cache/cacher/randomreplacementcache.go index 10944204..5dfb487a 100644 --- a/util/cache/cacher/randomreplacementcache.go +++ b/util/cache/cacher/randomreplacementcache.go @@ -47,7 +47,6 @@ func (cache *RandomReplacementCache) ForEach(cleanupFunc func(string, interface{ cache.removeWithoutLock(key) } } - } // It's useful to get the keys directly from the map instead of the array of keys diff --git a/util/cache/timesweptcache.go b/util/cache/timesweptcache.go index 99b39bbe..4d11a65a 100644 --- a/util/cache/timesweptcache.go +++ b/util/cache/timesweptcache.go @@ -1,6 +1,7 @@ package cache import ( + "fmt" "time" "github.com/untangle/golang-shared/services/logger" @@ -23,6 +24,7 @@ func NewTimeSweptCache(cache cacher.Cacher, waitTime time.Duration) *TimeSweptCa } func (sweeper *TimeSweptCache) StartSweeping(cleanupFunc func(string, interface{}) bool) { + fmt.Println("!!!!!!!!!!!!starting the sweeper") go sweeper.runCleanup(cleanupFunc) } @@ -33,6 +35,7 @@ func (sweeper *TimeSweptCache) runCleanup(cleanupFunc func(string, interface{}) sweeper.shutdownChannel <- true return case <-time.After(sweeper.waitTime): + fmt.Println("??????????????") sweeper.Cacher.ForEach(cleanupFunc) } } From 03489a6d3e5e49300d6a06ce61ef0cc378c8da08 Mon Sep 17 00:00:00 2001 From: Adam Briles Date: Wed, 31 Aug 2022 17:48:07 -0600 Subject: [PATCH 26/28] added doc blocks to public methods --- util/cache/cacher/cacher.go | 5 +- util/cache/cacher/lrucache.go | 51 +++++------------- util/cache/cacher/lrucache_test.go | 25 ++++----- util/cache/cacher/randomreplacementcache.go | 52 ++++++++----------- .../cacher/randomreplacementcache_test.go | 35 +++++-------- util/cache/timesweptcache.go | 16 ++++-- util/cache/timesweptcache_test.go | 1 + 7 files changed, 75 insertions(+), 110 deletions(-) diff --git a/util/cache/cacher/cacher.go b/util/cache/cacher/cacher.go index ba740211..9425a76e 100644 --- a/util/cache/cacher/cacher.go +++ b/util/cache/cacher/cacher.go @@ -1,15 +1,12 @@ package cacher -// Interface for structs implementing basic caching functionality +// Interface for structs implementing a basic caching functionality type Cacher interface { Get(string) (interface{}, bool) Put(string, interface{}) Clear() Remove(string) - // An iterator - //GetIterator() func() (string, interface{}, bool) - // Runs a given function on each cache element ForEach(func(string, interface{}) bool) } diff --git a/util/cache/cacher/lrucache.go b/util/cache/cacher/lrucache.go index 81ad43cb..e6ed4f0b 100644 --- a/util/cache/cacher/lrucache.go +++ b/util/cache/cacher/lrucache.go @@ -7,6 +7,8 @@ import ( "github.com/untangle/golang-shared/services/logger" ) +// Attaches the value being added to the cache with +// it's key used to it type KeyPair struct { Key string Value interface{} @@ -27,6 +29,8 @@ type LruCache struct { cacheName string } +// Returns a pointer to a newly initialized LruCache with it's capacity and name set to +// those provided by capactiy and cacheName, respectively func NewLruCache(capacity uint, cacheName string) *LruCache { return &LruCache{ capacity: capacity, @@ -36,6 +40,10 @@ func NewLruCache(capacity uint, cacheName string) *LruCache { } } +// Iterates over each key, value pair in the cache and runs them through +// the provided cleanup function. If the cleanup function provided returns true, +// The element will be removed from the cache. The cleanupFunction provided +// will be given the key and the value of the current element. func (cache *LruCache) ForEach(cleanupFunc func(string, interface{}) bool) { cache.cacheMutex.Lock() defer cache.cacheMutex.Unlock() @@ -67,40 +75,6 @@ func (cache *LruCache) Get(key string) (interface{}, bool) { return value, found } -// func (cache *LruCache) GetIterator() func() (string, interface{}, bool) { -// // Once an iterator has been retrieved, it captures the state of -// // of the cache. If the cache is updated the iterator won't contain -// // the update -// cache.cacheMutex.Lock() -// listSnapshot := copyLinkedList(cache.list) -// cache.cacheMutex.Unlock() - -// node := listSnapshot.Front() -// // Return key, val, and if there is anything left to iterate over -// return func() (string, interface{}, bool) { -// if node != nil { - -// currentNode := node -// currentKey := currentNode.Value.(*list.Element).Value.(KeyPair).Key -// currentValue := currentNode.Value.(*list.Element).Value.(KeyPair).Value - -// node = node.Next() - -// return currentKey, currentValue, true -// } else { -// return "", nil, false -// } -// } -// } - -// func copyLinkedList(original *list.List) *list.List { -// listCopy := list.New() -// for node := original.Front(); node != nil; node = node.Next() { -// listCopy.PushBack(node.Value) -// } -// return listCopy -// } - // Add an item to the cache and move it to the front of the queue. // If the item's key is already in the cache, update the key's value // and move the the item to the front of the queue. @@ -164,21 +138,24 @@ func (cache *LruCache) Clear() { logger.Debug("Cleared cache of name %s", cache.cacheName) } -func (cache *LruCache) GetMostRecentlyUsed() (interface{}, interface{}) { +// Gets the most recently looked up value in the cache +func (cache *LruCache) GetMostRecentlyUsed() (string, interface{}) { cache.cacheMutex.Lock() defer cache.cacheMutex.Unlock() keyPair := cache.list.Front().Value.(*list.Element).Value.(KeyPair) return keyPair.Key, keyPair.Value } -func (cache *LruCache) GetLeastRecentlyUsed() (interface{}, interface{}) { +// Get the least recently looked up value on the cache +func (cache *LruCache) GetLeastRecentlyUsed() (string, interface{}) { cache.cacheMutex.Lock() defer cache.cacheMutex.Unlock() keyPair := cache.list.Back().Value.(*list.Element).Value.(KeyPair) return keyPair.Key, keyPair.Value } -func (cache *LruCache) GetCurrentCapacity() int { +// Gets the total number of elements currently in the cache +func (cache *LruCache) GetTotalElements() int { cache.cacheMutex.Lock() defer cache.cacheMutex.Unlock() return cache.list.Len() diff --git a/util/cache/cacher/lrucache_test.go b/util/cache/cacher/lrucache_test.go index ed0c2bbc..11feeed4 100644 --- a/util/cache/cacher/lrucache_test.go +++ b/util/cache/cacher/lrucache_test.go @@ -28,18 +28,7 @@ func TestLruCacheTestSuite(t *testing.T) { suite.Run(t, new(LruCacheTestSuite)) } -// func (suite *LruCacheTestSuite) TestGetIterator() { -// next := suite.cache.GetIterator() - -// count := 0 -// for key, _, isNext := next(); isNext; key, _, isNext = next() { -// _, ok := suite.cache.Get(key) -// suite.True(ok, "The iterator returned a value not in the cache") -// count += 1 -// } -// suite.Equal(suite.cache.capacity, uint(count)) -// } - +// Tests getting the most recently fetched element from the cache func (suite *LruCacheTestSuite) TestGetMostRecentlyUsed() { expectedKey, expectedValue := "4", 4 key, value := suite.cache.GetMostRecentlyUsed() @@ -48,6 +37,7 @@ func (suite *LruCacheTestSuite) TestGetMostRecentlyUsed() { suite.Equal(expectedValue, value) } +// Tests getting the least recently used element from the cache func (suite *LruCacheTestSuite) TestGetLeastRecentlyUsed() { expectedKey, expectedValue := "0", 0 key, value := suite.cache.GetLeastRecentlyUsed() @@ -56,6 +46,7 @@ func (suite *LruCacheTestSuite) TestGetLeastRecentlyUsed() { suite.Equal(expectedValue, value) } +// Tests removing an element from the cache func (suite *LruCacheTestSuite) TestRemove() { toRemove := "2" _, ok := suite.cache.Get(toRemove) @@ -82,10 +73,12 @@ func (suite *LruCacheTestSuite) TestGet() { } } +// Tests getting the total number of elements in the cache func (suite *LruCacheTestSuite) TestGetCurrentCapacity() { - suite.Equal(int(suite.capacity), suite.cache.GetCurrentCapacity()) + suite.Equal(int(suite.capacity), suite.cache.GetTotalElements()) } +// Tests adding an element to the cache when the cache is at capacity func (suite *LruCacheTestSuite) TestCapacityExceeded() { // Check that the cache has something in it to start with @@ -99,6 +92,7 @@ func (suite *LruCacheTestSuite) TestCapacityExceeded() { suite.False(okAfterOverwritten, "The element with key %s was not overwritten in the cache", toRemove) } +// Tests writing to a value already in the cache func (suite *LruCacheTestSuite) TestUpdatingCacheValue() { toUpdate := "2" updatedValue := 10 @@ -118,11 +112,12 @@ func (suite *LruCacheTestSuite) TestUpdatingCacheValue() { suite.Equal(updatedValue, value) } +// Tests clearing the cache func (suite *LruCacheTestSuite) TestClear() { // Check that the cache has something in it to start with - suite.Equal(int(suite.capacity), suite.cache.GetCurrentCapacity(), "The cache is missing elements. It was not setup properly by SetupTest()") + suite.Equal(int(suite.capacity), suite.cache.GetTotalElements(), "The cache is missing elements. It was not setup properly by SetupTest()") suite.cache.Clear() - suite.Equal(0, suite.cache.GetCurrentCapacity(), "The cache was not successfully cleared") + suite.Equal(0, suite.cache.GetTotalElements(), "The cache was not successfully cleared") } diff --git a/util/cache/cacher/randomreplacementcache.go b/util/cache/cacher/randomreplacementcache.go index 5dfb487a..956dace5 100644 --- a/util/cache/cacher/randomreplacementcache.go +++ b/util/cache/cacher/randomreplacementcache.go @@ -7,15 +7,16 @@ import ( "github.com/untangle/golang-shared/services/logger" ) +// The value with it's corresponding index in the slice +// used to keep track of all the keys in the cache. type Value struct { keyIndex uint value interface{} } // Simple cache that removes elements randomly when the cache capacity is met. -// O(1) lookups and insertions. For O(1) insertions, space complexity had to be increased. -// The size of the cache will grow for every cache deletion since the keys slice can't -// have elements removed from it. +// O(1) lookups and insertions. For O(1) insertions, space complexity had to be increased +// by adding a few data structures for bookkeeping. // The cache can be read from by multiple threads, but written to by one. type RandomReplacementCache struct { maxCapacity uint @@ -23,10 +24,16 @@ type RandomReplacementCache struct { cacheName string cacheMutex sync.RWMutex + // Keys is a slice of all the keys in the cache + // Used to randomly select which item should be + // removed from the cache when the capacity is + // exceeded keys []string totalElements uint } +// Returns a pointer to a newly initialized RandomReplacement and sets its cache capacity +// and name to those provided by capacity and cacheName, respectively func NewRandomReplacementCache(capacity uint, cacheName string) *RandomReplacementCache { return &RandomReplacementCache{ maxCapacity: capacity, @@ -37,6 +44,9 @@ func NewRandomReplacementCache(capacity uint, cacheName string) *RandomReplaceme } } +// Iterates over each key, value pair in the cache and runs them through +// the provided cleanup function. If the cleanup function provided returns true, +// The element will be removed from the cache func (cache *RandomReplacementCache) ForEach(cleanupFunc func(string, interface{}) bool) { cache.cacheMutex.Lock() defer cache.cacheMutex.Unlock() @@ -63,30 +73,7 @@ func getMapKeys(m *map[string]*Value) []string { return keys } -// func (cache *RandomReplacementCache) GetIterator() func() (string, interface{}, bool) { -// // Once an iterator has been retrieved, it captures the state of -// // of the cache. If the cache is updated the iterator won't contain -// // the update -// cache.cacheMutex.RLock() -// keys := getMapKeys(&cache.elements) -// cache.cacheMutex.RUnlock() - -// i := 0 -// // Return key, val, and if there is anything left to iterate over -// return func() (string, interface{}, bool) { -// if i == len(keys) { -// return "", nil, false -// } - -// currentKey := keys[i] - -// // The value could be nil if the map was altered -// value, _ := cache.Get(currentKey) -// i += 1 -// return currentKey, &value, true -// } -// } - +// Retrieves a value from the cache corresponding to the provided key func (cache *RandomReplacementCache) Get(key string) (interface{}, bool) { cache.cacheMutex.RLock() defer cache.cacheMutex.RUnlock() @@ -98,6 +85,9 @@ func (cache *RandomReplacementCache) Get(key string) (interface{}, bool) { } } +// Places the provided value in the cache. If it already exists, the new value +// provided replaces the previous one. If the capacity of the cache has been met, +// an element from the cache is randomly deleted and the provided value is added. func (cache *RandomReplacementCache) Put(key string, value interface{}) { cache.cacheMutex.Lock() defer cache.cacheMutex.Unlock() @@ -124,6 +114,8 @@ func (cache *RandomReplacementCache) Put(key string, value interface{}) { } } +// Deletes an element from the cache. Does not acquire the mutex lock +// Any function calling this should acquire the mutex lock there func (cache *RandomReplacementCache) removeWithoutLock(key string) { if _, ok := cache.elements[key]; ok { indexToRemove := cache.elements[key].keyIndex @@ -143,13 +135,14 @@ func (cache *RandomReplacementCache) removeWithoutLock(key string) { // else the key didn't exists in the cache and nothing should be done } -// Remove is an O(n) operation since the key to be removed must be found first +// Removes an element from the cache. func (cache *RandomReplacementCache) Remove(key string) { cache.cacheMutex.Lock() defer cache.cacheMutex.Unlock() cache.removeWithoutLock(key) } +// Removes all elements from the cache func (cache *RandomReplacementCache) Clear() { cache.cacheMutex.Lock() defer cache.cacheMutex.Unlock() @@ -159,7 +152,8 @@ func (cache *RandomReplacementCache) Clear() { logger.Debug("Cleared cache of name %s", cache.cacheName) } -func (cache *RandomReplacementCache) GetCurrentCapacity() int { +// Returns the total elements currently in the cache +func (cache *RandomReplacementCache) GetTotalElements() int { cache.cacheMutex.RLock() defer cache.cacheMutex.RUnlock() return int(cache.totalElements) diff --git a/util/cache/cacher/randomreplacementcache_test.go b/util/cache/cacher/randomreplacementcache_test.go index 776e6103..89776492 100644 --- a/util/cache/cacher/randomreplacementcache_test.go +++ b/util/cache/cacher/randomreplacementcache_test.go @@ -30,6 +30,7 @@ func (suite *RRCacheTestSuite) SetupTest() { } } +// Test mutating elements in a RandomReplacement cache // The suite is not being used since // a pointer is required as a cache value // if any alteration to the value are going to @@ -46,7 +47,7 @@ func TestForEachElementMutation(t *testing.T) { testCache.Put(strconv.Itoa(int(i)), &newVal) } - assert.Equal(t, testCache.GetCurrentCapacity(), capacity) + assert.Equal(t, testCache.GetTotalElements(), capacity) mutateElement := func(s string, i interface{}) bool { deleteElement := false @@ -63,6 +64,7 @@ func TestForEachElementMutation(t *testing.T) { } } +// Test deleting elements using the ForEach method func (suite *RRCacheTestSuite) TestForEachElementDeletion() { suite.cache.ForEach(func(s string, i interface{}) bool { deleteElement := false @@ -78,23 +80,7 @@ func (suite *RRCacheTestSuite) TestForEachElementDeletion() { } } -// func (suite *RRCacheTestSuite) TestNextElement() { -// next := suite.cache.GetIterator() -// var key string -// var isNext bool - -// count := 0 -// for key, _, isNext = next(); isNext == true; key, _, isNext = next() { -// _, ok := suite.cache.elements[key] - -// suite.True(ok, "The iterator retrieved a value not in the cache") -// count += 1 -// } - -// suite.Equal(suite.capacity, uint(count), "The iterator did not iterate over ever element in the cache") - -// } - +// Test retrieving elements from the cache func (suite *RRCacheTestSuite) TestGet() { for i := 0; i < int(suite.capacity); i++ { value, ok := suite.cache.Get(strconv.Itoa(i)) @@ -104,6 +90,7 @@ func (suite *RRCacheTestSuite) TestGet() { } } +// Test updating an element in the cache func (suite *RRCacheTestSuite) TestUpdatingCacheValue() { toUpdate := "2" updatedValue := 10 @@ -118,14 +105,16 @@ func (suite *RRCacheTestSuite) TestUpdatingCacheValue() { suite.Equal(updatedValue, value) } +// Test clearing the cache func (suite *RRCacheTestSuite) TestClear() { - suite.Equal(int(suite.capacity), suite.cache.GetCurrentCapacity(), "The cache is missing elements. It was not setup properly by SetupTest()") + suite.Equal(int(suite.capacity), suite.cache.GetTotalElements(), "The cache is missing elements. It was not setup properly by SetupTest()") suite.cache.Clear() - suite.Equal(0, suite.cache.GetCurrentCapacity(), "The cache was not successfully cleared") + suite.Equal(0, suite.cache.GetTotalElements(), "The cache was not successfully cleared") } +// Test adding an element when the cache is already at capacity func (suite *RRCacheTestSuite) TestCapacityExceeded() { keysBeforePut := getMapKeys(&suite.cache.elements) newKey := "6" @@ -143,10 +132,12 @@ func (suite *RRCacheTestSuite) TestCapacityExceeded() { suite.NotEqual(keysAfterPut, keysBeforePut) } -func (suite *RRCacheTestSuite) TestGetCurrentCapacity() { - suite.Equal(int(suite.capacity), suite.cache.GetCurrentCapacity()) +// Test getting the total elements in the cache +func (suite *RRCacheTestSuite) TestGetTotalElements() { + suite.Equal(int(suite.capacity), suite.cache.GetTotalElements()) } +// Test removing elements from the cache func (suite *RRCacheTestSuite) TestRemove() { keyToRemove := "2" _, ok := suite.cache.Get(keyToRemove) diff --git a/util/cache/timesweptcache.go b/util/cache/timesweptcache.go index 4d11a65a..736130e5 100644 --- a/util/cache/timesweptcache.go +++ b/util/cache/timesweptcache.go @@ -1,13 +1,15 @@ package cache import ( - "fmt" "time" "github.com/untangle/golang-shared/services/logger" "github.com/untangle/golang-shared/util/cache/cacher" ) +// Adds the ability to sweep elements on a timer to a cache. +// A goroutine is started which sweeps through +// the underlying cache on a set interval. type TimeSweptCache struct { cacher.Cacher @@ -15,6 +17,8 @@ type TimeSweptCache struct { waitTime time.Duration } +// Returns a pointer to an initialized TimeSweptCache. The underlying cache type is set to +// what is provided by cache. The interval in which the sweeper runs is set by waitTime. func NewTimeSweptCache(cache cacher.Cacher, waitTime time.Duration) *TimeSweptCache { return &TimeSweptCache{ Cacher: cache, @@ -23,11 +27,17 @@ func NewTimeSweptCache(cache cacher.Cacher, waitTime time.Duration) *TimeSweptCa } } +// Starts sweeping the cache at the provided interval with the +// cleanupFunc provided. The cleanUp func will be provided with the key/val of +// each element in the cache. If the element should be deleted from the cache, +// return true. If the cache value is a pointer to an object, this method +// can be used to mutate a cache value. +// Does not do a sweep on call, only after waitTime. func (sweeper *TimeSweptCache) StartSweeping(cleanupFunc func(string, interface{}) bool) { - fmt.Println("!!!!!!!!!!!!starting the sweeper") go sweeper.runCleanup(cleanupFunc) } +// Runs the cleanup function func (sweeper *TimeSweptCache) runCleanup(cleanupFunc func(string, interface{}) bool) { for { select { @@ -35,12 +45,12 @@ func (sweeper *TimeSweptCache) runCleanup(cleanupFunc func(string, interface{}) sweeper.shutdownChannel <- true return case <-time.After(sweeper.waitTime): - fmt.Println("??????????????") sweeper.Cacher.ForEach(cleanupFunc) } } } +// Stops sweepign the cache func (sweeper *TimeSweptCache) StopSweeping() { sweeper.shutdownChannel <- true diff --git a/util/cache/timesweptcache_test.go b/util/cache/timesweptcache_test.go index 9c3e7f8d..9d427f85 100644 --- a/util/cache/timesweptcache_test.go +++ b/util/cache/timesweptcache_test.go @@ -67,6 +67,7 @@ func (suite *TestTimeSweptCache) SetupTest() { } } +// Test that the provided cleanup function to StartSweeping() is being ran func (suite *TestTimeSweptCache) TestCleanupTaskRan() { // Remove elements that aren't equal to 4 From 309a97e8deda040baf44bddf9c6684393d652d86 Mon Sep 17 00:00:00 2001 From: Adam Briles Date: Thu, 1 Sep 2022 08:08:55 -0600 Subject: [PATCH 27/28] added comments --- util/cache/cacher/lrucache.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/util/cache/cacher/lrucache.go b/util/cache/cacher/lrucache.go index e6ed4f0b..7bc46623 100644 --- a/util/cache/cacher/lrucache.go +++ b/util/cache/cacher/lrucache.go @@ -7,8 +7,7 @@ import ( "github.com/untangle/golang-shared/services/logger" ) -// Attaches the value being added to the cache with -// it's key used to it +// Attach the key, used to look up a value in the cache, to its value. type KeyPair struct { Key string Value interface{} From c488c09bd3913778c7fa22806bc97d513420bfd9 Mon Sep 17 00:00:00 2001 From: Adam Briles Date: Thu, 1 Sep 2022 10:09:46 -0600 Subject: [PATCH 28/28] Fixed data race in mock cache for timesweptcache unit tests --- util/cache/cacher/lrucache.go | 3 ++- util/cache/timesweptcache_test.go | 24 +++++++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/util/cache/cacher/lrucache.go b/util/cache/cacher/lrucache.go index 7bc46623..735c14a9 100644 --- a/util/cache/cacher/lrucache.go +++ b/util/cache/cacher/lrucache.go @@ -13,7 +13,7 @@ type KeyPair struct { Value interface{} } -// A simple LRU Cache implementation. The least recently used elements +// A simple LRU Cache implementation. The least recently used element // in the cache are removed if the cache's max capacity is hit. The cache's // mutex cannot be a RWMutex since Gets alter the cache's underlying data structures. // Slower concurrent performance than a Random Removal cache, but more cache hits. @@ -111,6 +111,7 @@ func (cache *LruCache) Put(key string, value interface{}) { } } +// Removes an element from the cache. // Does NOT take the cache's lock. Functions calling removeElementNoLock() // need to do it themselves func (cache *LruCache) removeElementNoLock(key string) { diff --git a/util/cache/timesweptcache_test.go b/util/cache/timesweptcache_test.go index 9d427f85..b412ec06 100644 --- a/util/cache/timesweptcache_test.go +++ b/util/cache/timesweptcache_test.go @@ -2,6 +2,7 @@ package cache import ( "strconv" + "sync" "testing" "time" @@ -9,36 +10,45 @@ import ( ) type MockCache struct { - elements map[string]interface{} + elements map[string]interface{} + cacheLock sync.RWMutex } func (cache *MockCache) Get(key string) (interface{}, bool) { + cache.cacheLock.RLock() + defer cache.cacheLock.RUnlock() val, ok := cache.elements[key] return val, ok } func (cache *MockCache) Put(key string, value interface{}) { + cache.cacheLock.Lock() + defer cache.cacheLock.Unlock() cache.elements[key] = value } func (cache *MockCache) Clear() { + cache.cacheLock.Lock() + defer cache.cacheLock.Unlock() cache.elements = make(map[string]interface{}) } func (cache *MockCache) Remove(key string) { - delete(cache.elements, key) + cache.cacheLock.Lock() + defer cache.cacheLock.Unlock() + cache.removeNoLock(key) } -func (cache *MockCache) GetIterator() func() (string, interface{}, bool) { - return func() (string, interface{}, bool) { - return "true", "true", false - } +func (cache *MockCache) removeNoLock(key string) { + delete(cache.elements, key) } func (cache *MockCache) ForEach(cleanUp func(string, interface{}) bool) { + cache.cacheLock.Lock() + defer cache.cacheLock.Unlock() for key, val := range cache.elements { if cleanUp(key, val) { - cache.Remove(key) + cache.removeNoLock(key) } } }