-
Notifications
You must be signed in to change notification settings - Fork 24
feat(core): add cache manager #2449
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 5 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
e5cf9b4
feat(core): add cache manager
jrschumacher 39afdb7
fix lint issues
jrschumacher 33feae1
Resolve some defects
jrschumacher ea045fd
Refactor ristretto config
jrschumacher 58e2d22
Update service/pkg/util/relativesizes.go
jrschumacher f7b97ba
Add generics
jrschumacher 1abcbae
Move config
jrschumacher ab1fec5
Update config
jrschumacher 96987c5
Update docs/Configuring.md
jakedoublev aaa456e
Update docs/Configuring.md
jakedoublev a4f3435
Update docs/Configuring.md
jakedoublev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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,130 @@ | ||
| package cache | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "strconv" | ||
| "time" | ||
|
|
||
| "github.com/dgraph-io/ristretto" | ||
| "github.com/eko/gocache/lib/v4/cache" | ||
| "github.com/eko/gocache/lib/v4/store" | ||
| ristretto_store "github.com/eko/gocache/store/ristretto/v4" | ||
| "github.com/opentdf/platform/service/logger" | ||
| ) | ||
|
|
||
| type Manager struct { | ||
jakedoublev marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| cache *cache.Cache[interface{}] | ||
| } | ||
|
|
||
| // Cache is a cache implementation using gocache | ||
| type Cache struct { | ||
| manager *Manager | ||
| serviceName string | ||
| cacheOptions Options | ||
| logger *logger.Logger | ||
| } | ||
|
|
||
| type Options struct { | ||
| Expiration time.Duration | ||
| Cost int64 | ||
| } | ||
|
|
||
| // NewCache creates a new Cache instance using Ristretto as the backend. | ||
| func NewCacheManager(maxCost int64) (*Manager, error) { | ||
| numCounters, bufferItems, err := EstimateRistrettoConfigParams(maxCost) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| config := &ristretto.Config{ | ||
| NumCounters: numCounters, // number of keys to track frequency of (10x max items) | ||
| MaxCost: maxCost, // maximum cost of cache (e.g., 1<<20 for 1MB) | ||
| BufferItems: bufferItems, // number of keys per Get buffer. | ||
| } | ||
| store, err := ristretto.NewCache(config) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| ristrettoStore := ristretto_store.NewRistretto(store) | ||
| return &Manager{ | ||
| cache: cache.New[interface{}](ristrettoStore), | ||
| }, nil | ||
| } | ||
|
|
||
| // NewCache creates a new Cache instance with the given service name and options. | ||
| // The purpose of this function is to create a new cache for a specific service. | ||
| // Because caching can be expensive we want to make sure there are some strict controls with | ||
| // how it is used. | ||
| func (c *Manager) NewCache(serviceName string, log *logger.Logger, options Options) (*Cache, error) { | ||
| if log == nil { | ||
| return nil, errors.New("logger cannot be nil") | ||
| } | ||
| cache := &Cache{ | ||
| manager: c, | ||
| serviceName: serviceName, | ||
| cacheOptions: options, | ||
| } | ||
| cache.logger = log. | ||
jrschumacher marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| With("subsystem", "cache"). | ||
| With("serviceTag", cache.getServiceTag()). | ||
| With("expiration", options.Expiration.String()). | ||
| With("cost", strconv.FormatInt(options.Cost, 10)) | ||
| cache.logger.Info("created cache") | ||
| return cache, nil | ||
| } | ||
|
|
||
| func (c *Cache) Get(ctx context.Context, key string) (interface{}, error) { | ||
| val, err := c.manager.cache.Get(ctx, c.getKey(key)) | ||
| if err != nil { | ||
| // All errors are a cache miss in the gocache library. | ||
jrschumacher marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| c.logger.Debug("cache miss", "key", key, "error", err) | ||
| return nil, err | ||
| } | ||
| c.logger.Debug("cache hit", "key", key) | ||
| return val, nil | ||
| } | ||
|
|
||
| func (c *Cache) Set(ctx context.Context, key string, object interface{}, tags []string) error { | ||
| tags = append(tags, c.getServiceTag()) | ||
| opts := []store.Option{ | ||
| store.WithTags(tags), | ||
| store.WithExpiration(c.cacheOptions.Expiration), | ||
| store.WithCost(c.cacheOptions.Cost), | ||
| } | ||
|
|
||
| err := c.manager.cache.Set(ctx, c.getKey(key), object, opts...) | ||
| if err != nil { | ||
| c.logger.Error("set error", "key", key, "error", err) | ||
| return err | ||
| } | ||
| c.logger.Debug("set cache", "key", key) | ||
| return nil | ||
| } | ||
|
|
||
| func (c *Cache) Invalidate(ctx context.Context) error { | ||
| err := c.manager.cache.Invalidate(ctx, store.WithInvalidateTags([]string{c.getServiceTag()})) | ||
| if err != nil { | ||
| c.logger.Error("invalidate error", "error", err) | ||
| return err | ||
| } | ||
| c.logger.Info("invalidate cache") | ||
| return nil | ||
| } | ||
|
|
||
| func (c *Cache) Delete(ctx context.Context, key string) error { | ||
| err := c.manager.cache.Delete(ctx, c.getKey(key)) | ||
| if err != nil { | ||
| c.logger.Error("delete error", "key", key, "error", err) | ||
| return err | ||
| } | ||
| c.logger.Info("delete cache", "key", key) | ||
| return nil | ||
| } | ||
|
|
||
| func (c *Cache) getKey(key string) string { | ||
| return c.serviceName + ":" + key | ||
| } | ||
|
|
||
| func (c *Cache) getServiceTag() string { | ||
| return "svc:" + c.serviceName | ||
| } | ||
This file contains hidden or 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,54 @@ | ||
| package cache | ||
|
|
||
| import ( | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/opentdf/platform/service/logger" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestNewCacheManager_ValidMaxCost(t *testing.T) { | ||
| maxCost := int64(1024 * 1024) // 1MB | ||
| manager, err := NewCacheManager(maxCost) | ||
| require.NoError(t, err) | ||
| require.NotNil(t, manager) | ||
| require.NotNil(t, manager.cache) | ||
| } | ||
|
|
||
| func TestNewCacheManager_InvalidMaxCost(t *testing.T) { | ||
| // Ristretto requires MaxCost > 0, so use 0 or negative | ||
| _, err := NewCacheManager(0) | ||
| require.Error(t, err) | ||
|
|
||
| _, err = NewCacheManager(-100) | ||
| require.Error(t, err) | ||
| } | ||
|
|
||
| func TestNewCacheManager_NewCacheIntegration(t *testing.T) { | ||
| maxCost := int64(1024 * 1024) | ||
| manager, err := NewCacheManager(maxCost) | ||
| require.NoError(t, err) | ||
| require.NotNil(t, manager) | ||
|
|
||
| // Use a simple logger stub | ||
| log, _ := newTestLogger() | ||
|
|
||
| options := Options{ | ||
| Expiration: 1 * time.Minute, | ||
| Cost: 1, | ||
| } | ||
| cache, err := manager.NewCache("testService", log, options) | ||
| require.NoError(t, err) | ||
| require.NotNil(t, cache) | ||
| require.Equal(t, "testService", cache.serviceName) | ||
| require.Equal(t, options, cache.cacheOptions) | ||
| } | ||
|
|
||
| // newTestLogger returns a logger.Logger stub for testing. | ||
jakedoublev marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| func newTestLogger() (*logger.Logger, func()) { | ||
| // If logger.Logger has a constructor that doesn't require external setup, use it. | ||
| // Otherwise, return a dummy or nil logger if allowed. | ||
| l, _ := logger.NewLogger(logger.Config{Output: "stdout", Level: "error", Type: "json"}) | ||
| return l, func() {} | ||
| } | ||
This file contains hidden or 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,55 @@ | ||
| package cache | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "runtime" | ||
| ) | ||
|
|
||
| const ( | ||
| // minimumNumCounters is the minimum number of counters for Ristretto cache | ||
| minimumNumCounters = 1000 | ||
| // maxCostFactor is the maximum cost factor for Ristretto cache | ||
| maxCostFactor = 10 // 10x max items | ||
| // maxAllowedCost is the maximum allowed cost for Ristretto cache (8GB) | ||
| maxAllowedCost = 8 * 1024 * 1024 * 1024 // 8GB | ||
| ) | ||
|
|
||
| // EstimateRistrettoConfigParams estimates Ristretto cache config parameters | ||
| // Uses a conservative default average item cost (1KB) if the true average is unknown. | ||
| func EstimateRistrettoConfigParams(maxCost int64) (int64, int64, error) { | ||
| if maxCost < 1 { | ||
| return 0, 0, fmt.Errorf("maxCost must be greater than 0, got %d", maxCost) | ||
| } | ||
| if maxCost > maxAllowedCost { | ||
| return 0, 0, fmt.Errorf("maxCost is unreasonably high (>%d): %d", maxAllowedCost, maxCost) | ||
| } | ||
| numCounters := ristrettoComputeNumCounters(maxCost) | ||
| bufferItems := ristrettoComputeBufferItems() | ||
| return numCounters, bufferItems, nil | ||
| } | ||
|
|
||
| // ristrettoComputeNumCounters calculates the recommended number of counters for the Ristretto cache | ||
| // based on the provided maximum cache cost (maxCost). It estimates the number of items by dividing | ||
| // maxCost by a default average item cost (1KB), then multiplies by a factor to determine the number | ||
| // of counters. The function ensures that the returned value is not less than a predefined minimum. | ||
| // This helps optimize cache performance and accuracy in eviction policies. | ||
| func ristrettoComputeNumCounters(maxCost int64) int64 { | ||
| const defaultAvgItemCost = 1024 // 1KB | ||
| numItems := maxCost / defaultAvgItemCost | ||
| if numItems < 1 { | ||
| numItems = 1 | ||
| } | ||
| numCounters := numItems * maxCostFactor | ||
| if numCounters < minimumNumCounters { | ||
| return minimumNumCounters | ||
| } | ||
| return numCounters | ||
| } | ||
|
|
||
| // ristrettoComputeBufferItems calculates the number of buffer items for the Ristretto cache. | ||
| // It multiplies a constant number of buffer items per writer by the number of CPUs available. | ||
| // This helps optimize throughput for concurrent cache writes. | ||
| func ristrettoComputeBufferItems() int64 { | ||
| const bufferItemsPerWriter = 64 | ||
| return bufferItemsPerWriter * int64(runtime.NumCPU()) | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.