Skip to content

Commit

Permalink
refactor: use groupedCache to optimize cache access (#944)
Browse files Browse the repository at this point in the history
* refactor: use groupedCache to optimize cache access

* refactor: fix review findings
  • Loading branch information
0xERR0R committed Mar 27, 2023
1 parent 8757dea commit 3b9fd7b
Show file tree
Hide file tree
Showing 11 changed files with 584 additions and 288 deletions.
78 changes: 78 additions & 0 deletions cache/stringcache/chained_grouped_cache.go
@@ -0,0 +1,78 @@
package stringcache

import (
"sort"

"golang.org/x/exp/maps"
)

type ChainedGroupedCache struct {
caches []GroupedStringCache
}

func NewChainedGroupedCache(caches ...GroupedStringCache) *ChainedGroupedCache {
return &ChainedGroupedCache{
caches: caches,
}
}

func (c *ChainedGroupedCache) ElementCount(group string) int {
sum := 0
for _, cache := range c.caches {
sum += cache.ElementCount(group)
}

return sum
}

func (c *ChainedGroupedCache) Contains(searchString string, groups []string) []string {
groupMatchedMap := make(map[string]struct{}, len(groups))

for _, cache := range c.caches {
for _, group := range cache.Contains(searchString, groups) {
groupMatchedMap[group] = struct{}{}
}
}

matchedGroups := maps.Keys(groupMatchedMap)

sort.Strings(matchedGroups)

return matchedGroups
}

func (c *ChainedGroupedCache) Refresh(group string) GroupFactory {
cacheFactories := make([]GroupFactory, len(c.caches))
for i, cache := range c.caches {
cacheFactories[i] = cache.Refresh(group)
}

return &chainedGroupFactory{
cacheFactories: cacheFactories,
}
}

type chainedGroupFactory struct {
cacheFactories []GroupFactory
}

func (c *chainedGroupFactory) AddEntry(entry string) {
for _, factory := range c.cacheFactories {
factory.AddEntry(entry)
}
}

func (c *chainedGroupFactory) Count() int {
var cnt int
for _, factory := range c.cacheFactories {
cnt += factory.Count()
}

return cnt
}

func (c *chainedGroupFactory) Finish() {
for _, factory := range c.cacheFactories {
factory.Finish()
}
}
94 changes: 94 additions & 0 deletions cache/stringcache/chained_grouped_cache_test.go
@@ -0,0 +1,94 @@
package stringcache_test

import (
"github.com/0xERR0R/blocky/cache/stringcache"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("Chained grouped cache", func() {
Describe("Empty cache", func() {
When("empty cache was created", func() {
cache := stringcache.NewChainedGroupedCache()

It("should have element count of 0", func() {
Expect(cache.ElementCount("someGroup")).Should(BeNumerically("==", 0))
})

It("should not find any string", func() {
Expect(cache.Contains("searchString", []string{"someGroup"})).Should(BeEmpty())
})
})
})
Describe("Delegation", func() {
When("Chained cache contains delegates", func() {
inMemoryCache1 := stringcache.NewInMemoryGroupedStringCache()
inMemoryCache2 := stringcache.NewInMemoryGroupedStringCache()
cache := stringcache.NewChainedGroupedCache(inMemoryCache1, inMemoryCache2)

factory := cache.Refresh("group1")

factory.AddEntry("string1")
factory.AddEntry("string2")

It("cache should still have 0 element, since finish was not executed", func() {
Expect(cache.ElementCount("group1")).Should(BeNumerically("==", 0))
})

It("factory has 4 elements (both caches)", func() {
Expect(factory.Count()).Should(BeNumerically("==", 4))
})

It("should have element count of 4", func() {
factory.Finish()
Expect(cache.ElementCount("group1")).Should(BeNumerically("==", 4))
})

It("should find strings", func() {
Expect(cache.Contains("string1", []string{"group1"})).Should(ConsistOf("group1"))
Expect(cache.Contains("string2", []string{"group1", "someOtherGroup"})).Should(ConsistOf("group1"))
})
})
})

Describe("Cache refresh", func() {
When("cache with 2 groups was created", func() {
inMemoryCache1 := stringcache.NewInMemoryGroupedStringCache()
inMemoryCache2 := stringcache.NewInMemoryGroupedStringCache()
cache := stringcache.NewChainedGroupedCache(inMemoryCache1, inMemoryCache2)

factory := cache.Refresh("group1")

factory.AddEntry("g1")
factory.AddEntry("both")
factory.Finish()

factory = cache.Refresh("group2")
factory.AddEntry("g2")
factory.AddEntry("both")
factory.Finish()

It("should contain 4 elements in 2 groups", func() {
Expect(cache.ElementCount("group1")).Should(BeNumerically("==", 4))
Expect(cache.ElementCount("group2")).Should(BeNumerically("==", 4))
Expect(cache.Contains("g1", []string{"group1", "group2"})).Should(ConsistOf("group1"))
Expect(cache.Contains("g2", []string{"group1", "group2"})).Should(ConsistOf("group2"))
Expect(cache.Contains("both", []string{"group1", "group2"})).Should(ConsistOf("group1", "group2"))
})

It("Should replace group content on refresh", func() {
factory := cache.Refresh("group1")
factory.AddEntry("newString")
factory.Finish()

Expect(cache.ElementCount("group1")).Should(BeNumerically("==", 2))
Expect(cache.ElementCount("group2")).Should(BeNumerically("==", 4))
Expect(cache.Contains("g1", []string{"group1", "group2"})).Should(BeEmpty())
Expect(cache.Contains("newString", []string{"group1", "group2"})).Should(ConsistOf("group1"))
Expect(cache.Contains("g2", []string{"group1", "group2"})).Should(ConsistOf("group2"))
Expect(cache.Contains("both", []string{"group1", "group2"})).Should(ConsistOf("group2"))
})
})

})
})
25 changes: 25 additions & 0 deletions cache/stringcache/grouped_cache_interface.go
@@ -0,0 +1,25 @@
package stringcache

type GroupedStringCache interface {
// Contains checks if one or more groups in the cache contains the search string.
// Returns group(s) containing the string or empty slice if string was not found
Contains(searchString string, groups []string) []string

// Refresh creates new factory for the group to be refreshed.
// Calling Finish on the factory will perform the group refresh.
Refresh(group string) GroupFactory

// ElementCount returns the amount of elements in the group
ElementCount(group string) int
}

type GroupFactory interface {
// AddEntry adds a new string to the factory to be added later to the cache groups.
AddEntry(entry string)

// Count returns amount of processed string in the factory
Count() int

// Finish replaces the group in cache with factory's content
Finish()
}
82 changes: 82 additions & 0 deletions cache/stringcache/in_memory_grouped_cache.go
@@ -0,0 +1,82 @@
package stringcache

import "sync"

type stringCacheFactoryFn func() cacheFactory

type InMemoryGroupedCache struct {
caches map[string]stringCache
lock sync.RWMutex
factoryFn stringCacheFactoryFn
}

func NewInMemoryGroupedStringCache() *InMemoryGroupedCache {
return &InMemoryGroupedCache{
caches: make(map[string]stringCache),
factoryFn: newStringCacheFactory,
}
}

func NewInMemoryGroupedRegexCache() *InMemoryGroupedCache {
return &InMemoryGroupedCache{
caches: make(map[string]stringCache),
factoryFn: newRegexCacheFactory,
}
}

func (c *InMemoryGroupedCache) ElementCount(group string) int {
c.lock.RLock()
cache, found := c.caches[group]
c.lock.RUnlock()

if !found {
return 0
}

return cache.elementCount()
}

func (c *InMemoryGroupedCache) Contains(searchString string, groups []string) []string {
var result []string

for _, group := range groups {
c.lock.RLock()
cache, found := c.caches[group]
c.lock.RUnlock()

if found && cache.contains(searchString) {
result = append(result, group)
}
}

return result
}

func (c *InMemoryGroupedCache) Refresh(group string) GroupFactory {
return &inMemoryGroupFactory{
factory: c.factoryFn(),
finishFn: func(sc stringCache) {
c.lock.Lock()
c.caches[group] = sc
c.lock.Unlock()
},
}
}

type inMemoryGroupFactory struct {
factory cacheFactory
finishFn func(stringCache)
}

func (c *inMemoryGroupFactory) AddEntry(entry string) {
c.factory.addEntry(entry)
}

func (c *inMemoryGroupFactory) Count() int {
return c.factory.count()
}

func (c *inMemoryGroupFactory) Finish() {
sc := c.factory.create()
c.finishFn(sc)
}

0 comments on commit 3b9fd7b

Please sign in to comment.