-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add facts cache package * Add FactGathererWithCache interface * Use cache in cibadmin gatherer * Add a pure GetOrUpdate funcion to cache pkg * Memoize the updateFunc * Improve GetOrUpdate function docstring
- Loading branch information
Showing
8 changed files
with
387 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
package factscache | ||
|
||
import ( | ||
"sync" | ||
|
||
"golang.org/x/sync/singleflight" | ||
|
||
log "github.com/sirupsen/logrus" | ||
) | ||
|
||
type UpdateCacheFunc func(args ...interface{}) (interface{}, error) | ||
|
||
type FactsCache struct { | ||
entries sync.Map | ||
group singleflight.Group | ||
} | ||
|
||
type Entry struct { | ||
content interface{} | ||
err error | ||
} | ||
|
||
func NewFactsCache() *FactsCache { | ||
return &FactsCache{ | ||
entries: sync.Map{}, | ||
group: singleflight.Group{}, | ||
} | ||
} | ||
|
||
// GetOrUpdate Runs FactsCache GetOrUpdate with a provided cache | ||
// If the cache is nil, it runs the function, otherwise it returns | ||
// from cache | ||
func GetOrUpdate( | ||
cache *FactsCache, | ||
entry string, | ||
udpateFunc UpdateCacheFunc, | ||
updateFuncArgs ...interface{}, | ||
) (interface{}, error) { | ||
if cache == nil { | ||
return udpateFunc(updateFuncArgs...) | ||
} | ||
|
||
return cache.GetOrUpdate( | ||
entry, | ||
udpateFunc, | ||
updateFuncArgs..., | ||
) | ||
} | ||
|
||
// Entries returns the cached entries list | ||
func (c *FactsCache) Entries() []string { | ||
keys := []string{} | ||
c.entries.Range(func(key, _ any) bool { | ||
// nolint:forcetypeassert | ||
keys = append(keys, key.(string)) | ||
return true | ||
}) | ||
return keys | ||
} | ||
|
||
// GetOrUpdate returns the cached result providing an entry name | ||
// or runs the updateFunc to generate the entry. | ||
// It locks its usage for each used key, returning the same value of the | ||
// first execution in the additional usages. | ||
// If other function with a different key is asked, it runs in parallel | ||
// without blocking. | ||
func (c *FactsCache) GetOrUpdate( | ||
entry string, | ||
udpateFunc UpdateCacheFunc, | ||
updateFuncArgs ...interface{}, | ||
) (interface{}, error) { | ||
loadedEntry, hit := c.entries.Load(entry) | ||
if hit { | ||
// nolint:forcetypeassert | ||
cacheEntry := loadedEntry.(Entry) | ||
log.Debugf("Value for entry %s already cached", entry) | ||
return cacheEntry.content, cacheEntry.err | ||
} | ||
|
||
// singleflight is used to avoid a duplicated function execution at | ||
// the same moment for a given key (memoization). | ||
// This way, the code only blocks the execution based on same keys, | ||
// not blocking other keys execution | ||
content, err, _ := c.group.Do(entry, func() (interface{}, error) { | ||
content, err := udpateFunc(updateFuncArgs...) | ||
newEntry := Entry{ | ||
content: content, | ||
err: err, | ||
} | ||
c.entries.Store(entry, newEntry) | ||
|
||
return content, err | ||
}) | ||
|
||
if err != nil { | ||
log.Debugf("New value with error set for entry %s", entry) | ||
return content, err | ||
} | ||
|
||
log.Debugf("New value for entry %s set", entry) | ||
return content, err | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
package factscache_test | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
"time" | ||
|
||
"github.com/stretchr/testify/suite" | ||
"github.com/trento-project/agent/internal/factsengine/factscache" | ||
"golang.org/x/sync/errgroup" | ||
) | ||
|
||
type FactsCacheTestSuite struct { | ||
suite.Suite | ||
returnValue string | ||
count int | ||
} | ||
|
||
func TestFactsCacheTestSuite(t *testing.T) { | ||
suite.Run(t, new(FactsCacheTestSuite)) | ||
} | ||
|
||
func (suite *FactsCacheTestSuite) SetupSuite() { | ||
suite.returnValue = "value" | ||
} | ||
|
||
func (suite *FactsCacheTestSuite) SetupTest() { | ||
suite.count = 0 | ||
} | ||
|
||
// nolint:errcheck | ||
func (suite *FactsCacheTestSuite) TestEntries() { | ||
cache := factscache.NewFactsCache() | ||
cache.GetOrUpdate("entry1", func(args ...interface{}) (interface{}, error) { | ||
return "", nil | ||
}) | ||
cache.GetOrUpdate("entry2", func(args ...interface{}) (interface{}, error) { | ||
return "", nil | ||
}) | ||
entries := cache.Entries() | ||
|
||
suite.ElementsMatch([]string{"entry1", "entry2"}, entries) | ||
} | ||
|
||
func (suite *FactsCacheTestSuite) TestGetOrUpdate() { | ||
cache := factscache.NewFactsCache() | ||
|
||
updateFunc := func(args ...interface{}) (interface{}, error) { | ||
return suite.returnValue, nil | ||
} | ||
|
||
value, err := cache.GetOrUpdate("entry1", updateFunc) | ||
|
||
suite.Equal(suite.returnValue, value) | ||
suite.NoError(err) | ||
} | ||
|
||
func (suite *FactsCacheTestSuite) TestGetOrUpdateWithError() { | ||
cache := factscache.NewFactsCache() | ||
someError := "some error" | ||
|
||
updateFunc := func(args ...interface{}) (interface{}, error) { | ||
return nil, fmt.Errorf(someError) | ||
} | ||
|
||
_, err := cache.GetOrUpdate("entry", updateFunc) | ||
|
||
suite.EqualError(err, someError) | ||
} | ||
|
||
func (suite *FactsCacheTestSuite) TestGetOrUpdateCacheHit() { | ||
cache := factscache.NewFactsCache() | ||
|
||
updateFunc := func(args ...interface{}) (interface{}, error) { | ||
suite.count++ | ||
return suite.returnValue, nil | ||
} | ||
|
||
// nolint:errcheck | ||
cache.GetOrUpdate("entry", updateFunc) | ||
value, err := cache.GetOrUpdate("entry", updateFunc) | ||
|
||
suite.Equal(suite.returnValue, value) | ||
suite.Equal(1, suite.count) | ||
suite.NoError(err) | ||
} | ||
|
||
func (suite *FactsCacheTestSuite) TestGetOrUpdateWithArgs() { | ||
cache := factscache.NewFactsCache() | ||
|
||
// nolint:forcetypeassert | ||
updateFunc := func(args ...interface{}) (interface{}, error) { | ||
arg1 := args[0].(int) | ||
arg2 := args[1].(string) | ||
return fmt.Sprintf("%d_%s", arg1, arg2), nil | ||
} | ||
|
||
value, err := cache.GetOrUpdate("entry", updateFunc, 1, "text") | ||
|
||
suite.Equal("1_text", value) | ||
suite.NoError(err) | ||
} | ||
|
||
// nolint:errcheck | ||
func (suite *FactsCacheTestSuite) TestGetOrUpdateCacheConcurrent() { | ||
cache := factscache.NewFactsCache() | ||
g := errgroup.Group{} | ||
|
||
updateFunc := func(args ...interface{}) (interface{}, error) { | ||
value, _ := args[0].(string) | ||
time.Sleep(100 * time.Millisecond) | ||
return value, nil | ||
} | ||
|
||
g.Go(func() error { | ||
value, _ := cache.GetOrUpdate("entry1", updateFunc, "initialValueEntry1") | ||
castedValue, _ := value.(string) | ||
suite.Equal("initialValueEntry1", castedValue) | ||
return nil | ||
}) | ||
g.Go(func() error { | ||
value, _ := cache.GetOrUpdate("entry2", updateFunc, "initialValueEntry2") | ||
castedValue, _ := value.(string) | ||
suite.Equal("initialValueEntry2", castedValue) | ||
return nil | ||
}) | ||
time.Sleep(50 * time.Millisecond) | ||
// The next 2 calls return the memoized value | ||
g.Go(func() error { | ||
value, _ := cache.GetOrUpdate("entry1", updateFunc, "newValueEntry1") | ||
castedValue, _ := value.(string) | ||
suite.Equal("initialValueEntry1", castedValue) | ||
return nil | ||
}) | ||
|
||
g.Go(func() error { | ||
value, _ := cache.GetOrUpdate("entry2", updateFunc, "newValueEntry2") | ||
castedValue, _ := value.(string) | ||
suite.Equal("initialValueEntry2", castedValue) | ||
return nil | ||
}) | ||
g.Wait() | ||
} | ||
|
||
func (suite *FactsCacheTestSuite) TestPureGetOrUpdate() { | ||
updateFunc := func(args ...interface{}) (interface{}, error) { | ||
suite.count++ | ||
return suite.returnValue, nil | ||
} | ||
|
||
value, err := factscache.GetOrUpdate(nil, "entry1", updateFunc) | ||
|
||
suite.Equal(suite.returnValue, value) | ||
suite.Equal(1, suite.count) | ||
suite.NoError(err) | ||
} | ||
|
||
func (suite *FactsCacheTestSuite) TestPureGetOrUpdateCacheHit() { | ||
cache := factscache.NewFactsCache() | ||
|
||
updateFunc := func(args ...interface{}) (interface{}, error) { | ||
suite.count++ | ||
return suite.returnValue, nil | ||
} | ||
|
||
// nolint:errcheck | ||
factscache.GetOrUpdate(cache, "entry1", updateFunc) | ||
value, err := factscache.GetOrUpdate(cache, "entry1", updateFunc) | ||
|
||
suite.Equal(suite.returnValue, value) | ||
suite.Equal(1, suite.count) | ||
suite.NoError(err) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.