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/util/cache/cacher/cacher.go b/util/cache/cacher/cacher.go new file mode 100644 index 00000000..9425a76e --- /dev/null +++ b/util/cache/cacher/cacher.go @@ -0,0 +1,12 @@ +package cacher + +// Interface for structs implementing a basic caching functionality +type Cacher interface { + Get(string) (interface{}, bool) + Put(string, interface{}) + Clear() + Remove(string) + + // 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 new file mode 100644 index 00000000..735c14a9 --- /dev/null +++ b/util/cache/cacher/lrucache.go @@ -0,0 +1,162 @@ +package cacher + +import ( + "container/list" + "sync" + + "github.com/untangle/golang-shared/services/logger" +) + +// Attach the key, used to look up a value in the cache, to its value. +type KeyPair struct { + Key string + Value interface{} +} + +// 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. +// 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 +} + +// 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, + list: list.New(), + elements: make(map[string]*list.Element), + cacheName: cacheName, + } +} + +// 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() + + for key, val := range cache.elements { + // Remove element if the cleanUp func returns true + if cleanupFunc(key, val.Value.(*list.Element).Value.(KeyPair).Value) { + cache.removeElementNoLock(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. +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} + + 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) + } +} + +// 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) { + 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) + } +} + +// Delete an item from the cache based off the key +func (cache *LruCache) Remove(key string) { + cache.cacheMutex.Lock() + defer cache.cacheMutex.Unlock() + cache.removeElementNoLock(key) +} + +// 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) +} + +// 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 +} + +// 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 +} + +// 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 new file mode 100644 index 00000000..11feeed4 --- /dev/null +++ b/util/cache/cacher/lrucache_test.go @@ -0,0 +1,123 @@ +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)) +} + +// Tests getting the most recently fetched element from the cache +func (suite *LruCacheTestSuite) TestGetMostRecentlyUsed() { + expectedKey, expectedValue := "4", 4 + key, value := suite.cache.GetMostRecentlyUsed() + + suite.Equal(expectedKey, key) + 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() + + suite.Equal(expectedKey, key) + suite.Equal(expectedValue, value) +} + +// Tests removing an element from the cache +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) + } +} + +// Tests getting the total number of elements in the cache +func (suite *LruCacheTestSuite) TestGetCurrentCapacity() { + 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 + + // 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) +} + +// Tests writing to a value already in the cache +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) +} + +// 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.GetTotalElements(), "The cache is missing elements. It was not setup properly by SetupTest()") + + suite.cache.Clear() + + 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 new file mode 100644 index 00000000..956dace5 --- /dev/null +++ b/util/cache/cacher/randomreplacementcache.go @@ -0,0 +1,160 @@ +package cacher + +import ( + "math/rand" + "sync" + + "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 +// 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 + elements map[string]*Value + 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, + elements: make(map[string]*Value, capacity), + cacheName: cacheName, + keys: make([]string, capacity), + totalElements: 0, + } +} + +// 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() + + for key, val := range cache.elements { + // Remove element if the cleanUp func returns true + if cleanupFunc(key, val.value) { + 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 +} + +// 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() + + if value, ok := cache.elements[key]; ok { + return value.value, ok + } else { + return nil, ok + } +} + +// 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() + + // Update element if already present in cache + if _, ok := cache.elements[key]; ok { + 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 cache.totalElements >= cache.maxCapacity { + indexToSwap := rand.Intn(len(cache.keys)) + keyToRemove := cache.keys[indexToSwap] + cache.removeWithoutLock(keyToRemove) + + logger.Debug("Removed element with key %s from the cache named %s", key, cache.cacheName) + } + + // Add new element + 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) + } +} + +// 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 + + // 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[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 +} + +// 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() + 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) +} + +// 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 new file mode 100644 index 00000000..89776492 --- /dev/null +++ b/util/cache/cacher/randomreplacementcache_test.go @@ -0,0 +1,151 @@ +package cacher + +import ( + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +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) + } +} + +// 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 +// 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.GetTotalElements(), capacity) + + mutateElement := func(s string, i interface{}) bool { + deleteElement := false + if *(i).(*int) != 3 { + *(i).(*int) = 3 + } + + return deleteElement + } + testCache.ForEach(mutateElement) + + for key, val := range testCache.elements { + assert.Equal(t, 3, *(val.value.(*int)), "The key %s was not altered as expected", key) + } +} + +// Test deleting elements using the ForEach method +func (suite *RRCacheTestSuite) TestForEachElementDeletion() { + suite.cache.ForEach(func(s string, i interface{}) bool { + deleteElement := false + if i.(int) < 4 { + deleteElement = true + } + + return deleteElement + }) + + for key, val := range suite.cache.elements { + suite.Equal(4, val.value.(int), "The key %s was not altered as expected", key) + } +} + +// 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)) + suite.True(ok, "The key %d did not exist in the cache", i) + + suite.Equal(value, i) + } +} + +// Test updating an element in the cache +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) +} + +// Test clearing the cache +func (suite *RRCacheTestSuite) TestClear() { + 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.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" + newVal := 6 + + suite.cache.Put("6", 6) + keysAfterPut := getMapKeys(&suite.cache.elements) + + suite.Equal(int(suite.capacity), len(suite.cache.elements)) + + 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) +} + +// 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) + 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, suite.cache.totalElements) +} diff --git a/util/cache/timesweptcache.go b/util/cache/timesweptcache.go new file mode 100644 index 00000000..736130e5 --- /dev/null +++ b/util/cache/timesweptcache.go @@ -0,0 +1,63 @@ +package cache + +import ( + "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 + + shutdownChannel chan bool + 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, + shutdownChannel: make(chan bool), + waitTime: waitTime, + } +} + +// 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) { + go sweeper.runCleanup(cleanupFunc) +} + +// Runs the cleanup function +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) + } + } +} + +// Stops sweepign the cache +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/util/cache/timesweptcache_test.go b/util/cache/timesweptcache_test.go new file mode 100644 index 00000000..b412ec06 --- /dev/null +++ b/util/cache/timesweptcache_test.go @@ -0,0 +1,105 @@ +package cache + +import ( + "strconv" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/suite" +) + +type MockCache struct { + 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) { + cache.cacheLock.Lock() + defer cache.cacheLock.Unlock() + cache.removeNoLock(key) +} + +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.removeNoLock(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) + } +} + +// Test that the provided cleanup function to StartSweeping() is being ran +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() +}