-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
21abca6
commit 6d22209
Showing
13 changed files
with
664 additions
and
2 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package cache | ||
|
||
import ( | ||
"time" | ||
|
||
"github.com/rudderlabs/rudder-go-kit/featureflags/provider" | ||
) | ||
|
||
// CacheEntry represents a cached item with its value and last updated timestamp | ||
type CacheEntry struct { | ||
Value map[string]*provider.FeatureValue | ||
LastUpdated time.Time | ||
} | ||
|
||
// CacheGetResult extends CacheEntry with staleness information | ||
type CacheGetResult struct { | ||
*CacheEntry | ||
IsStale bool | ||
} | ||
|
||
// Cache defines the interface for cache operations | ||
type Cache interface { | ||
Get(key string) (*CacheGetResult, bool) | ||
Set(key string, value map[string]*provider.FeatureValue) (time.Time, error) | ||
Delete(key string) error | ||
Clear() error | ||
IsEnabled() bool | ||
} | ||
|
||
// CacheError represents cache-specific errors | ||
type CacheError struct { | ||
Message string | ||
} | ||
|
||
func (e *CacheError) Error() string { | ||
return e.Message | ||
} | ||
|
||
// CacheConfig holds configuration for the cache | ||
type CacheConfig struct { | ||
Enabled bool | ||
TTLInSeconds int64 | ||
} |
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,70 @@ | ||
package cache_test | ||
|
||
import ( | ||
"testing" | ||
"time" | ||
|
||
"github.com/rudderlabs/rudder-go-kit/featureflags/cache" | ||
"github.com/rudderlabs/rudder-go-kit/featureflags/provider" | ||
) | ||
|
||
func TestMemoryCache(t *testing.T) { | ||
config := cache.CacheConfig{ | ||
Enabled: true, | ||
TTLInSeconds: 1, | ||
} | ||
|
||
c := cache.NewMemoryCache(config) | ||
|
||
testValue := map[string]*provider.FeatureValue{ | ||
"key1": { | ||
Value: "value1", | ||
}, | ||
} | ||
|
||
// Test Set and Get | ||
setTime, err := c.Set("key1", testValue) | ||
if err != nil { | ||
t.Errorf("Failed to set cache: %v", err) | ||
} | ||
|
||
result, ok := c.Get("key1") | ||
if !ok { | ||
t.Error("Expected cache hit, got miss") | ||
} | ||
if result.LastUpdated != setTime { | ||
t.Error("LastUpdated time mismatch") | ||
} | ||
|
||
// Test staleness | ||
time.Sleep(2 * time.Second) | ||
result, ok = c.Get("key1") | ||
if !ok { | ||
t.Error("Expected stale cache entry") | ||
} | ||
|
||
// Test Delete | ||
err = c.Delete("key1") | ||
if err != nil { | ||
t.Errorf("Failed to delete cache: %v", err) | ||
} | ||
|
||
result, ok = c.Get("key1") | ||
if ok { | ||
t.Error("Expected cache miss after delete") | ||
} | ||
|
||
// Test Clear | ||
c.Set("key1", testValue) | ||
c.Set("key2", testValue) | ||
|
||
err = c.Clear() | ||
if err != nil { | ||
t.Errorf("Failed to clear cache: %v", err) | ||
} | ||
|
||
result, ok = c.Get("key1") | ||
if ok { | ||
t.Error("Expected cache miss after clear") | ||
} | ||
} |
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,91 @@ | ||
package cache | ||
|
||
import ( | ||
"sync" | ||
"time" | ||
|
||
"github.com/rudderlabs/rudder-go-kit/featureflags/provider" | ||
) | ||
|
||
type MemoryCache struct { | ||
entries map[string]CacheEntry | ||
config CacheConfig | ||
mu sync.RWMutex | ||
} | ||
|
||
func NewMemoryCache(config CacheConfig) *MemoryCache { | ||
return &MemoryCache{ | ||
entries: make(map[string]CacheEntry), | ||
config: config, | ||
} | ||
} | ||
|
||
func (c *MemoryCache) Get(key string) (*CacheGetResult, bool) { | ||
if !c.IsEnabled() { | ||
return nil, false | ||
} | ||
|
||
c.mu.RLock() | ||
defer c.mu.RUnlock() | ||
|
||
entry, exists := c.entries[key] | ||
if !exists { | ||
return nil, false | ||
} | ||
|
||
isStale := false | ||
if c.config.TTLInSeconds > 0 { | ||
expirationTime := entry.LastUpdated.Add(time.Duration(c.config.TTLInSeconds) * time.Second) | ||
isStale = time.Now().After(expirationTime) | ||
} | ||
|
||
return &CacheGetResult{ | ||
CacheEntry: &entry, | ||
IsStale: isStale, | ||
}, true | ||
} | ||
|
||
func (c *MemoryCache) Set(key string, value map[string]*provider.FeatureValue) (time.Time, error) { | ||
if !c.IsEnabled() { | ||
return time.Time{}, &CacheError{Message: "Cache is disabled"} | ||
} | ||
|
||
c.mu.Lock() | ||
defer c.mu.Unlock() | ||
|
||
now := time.Now() | ||
c.entries[key] = CacheEntry{ | ||
Value: value, | ||
LastUpdated: now, | ||
} | ||
|
||
return now, nil | ||
} | ||
|
||
func (c *MemoryCache) Delete(key string) error { | ||
if !c.IsEnabled() { | ||
return &CacheError{Message: "Cache is disabled"} | ||
} | ||
|
||
c.mu.Lock() | ||
defer c.mu.Unlock() | ||
|
||
delete(c.entries, key) | ||
return nil | ||
} | ||
|
||
func (c *MemoryCache) Clear() error { | ||
if !c.IsEnabled() { | ||
return &CacheError{Message: "Cache is disabled"} | ||
} | ||
|
||
c.mu.Lock() | ||
defer c.mu.Unlock() | ||
|
||
c.entries = make(map[string]CacheEntry) | ||
return nil | ||
} | ||
|
||
func (c *MemoryCache) IsEnabled() bool { | ||
return c.config.Enabled | ||
} |
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,161 @@ | ||
package featureflags | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"sync" | ||
|
||
"github.com/rudderlabs/rudder-go-kit/featureflags/cache" | ||
"github.com/rudderlabs/rudder-go-kit/featureflags/provider" | ||
) | ||
|
||
// client represents the feature flag client interface | ||
type client interface { | ||
IsFeatureEnabled(workspaceID string, feature string) (bool, error) | ||
IsFeatureEnabledLatest(workspaceID string, feature string) (bool, error) | ||
GetFeatureValue(workspaceID string, feature string) (provider.FeatureValue, error) | ||
GetFeatureValueLatest(workspaceID string, feature string) (provider.FeatureValue, error) | ||
SetDefaultTraits(traits map[string]string) | ||
} | ||
|
||
var ( | ||
ffclient client | ||
clientOnce sync.Once | ||
) | ||
|
||
// getFeatureFlagClient returns the singleton feature flag client instance | ||
func getFeatureFlagClient() client { | ||
clientOnce.Do(func() { | ||
// read the api key from env vars and create the default cache config | ||
apiKey := os.Getenv("FLAGSMITH_SERVER_SIDE_ENVIRONMENT_KEY") | ||
if apiKey == "" { | ||
panic("FLAGSMITH_SERVER_SIDE_ENVIRONMENT_KEY is not set") | ||
} | ||
defaultCacheConfig := cache.CacheConfig{ | ||
Enabled: true, | ||
TTLInSeconds: 60, | ||
} | ||
|
||
// create the provider | ||
provider, err := provider.NewProvider(provider.ProviderConfig{ | ||
Type: "flagsmith", | ||
ApiKey: apiKey, | ||
}) | ||
if err != nil { | ||
panic(err) | ||
} | ||
ffclient = &clientImpl{ | ||
provider: provider, | ||
cache: cache.NewMemoryCache(defaultCacheConfig), | ||
} | ||
}) | ||
return ffclient | ||
} | ||
|
||
type clientImpl struct { | ||
provider provider.Provider | ||
cache cache.Cache | ||
defaultTraits map[string]string | ||
} | ||
|
||
// IsFeatureEnabled checks if a feature is enabled for a workspace | ||
// Note: Result may be stale if returned from cache. Use IsFeatureEnabledLatest if stale values are not acceptable. | ||
func (c *clientImpl) IsFeatureEnabled(workspaceID string, feature string) (bool, error) { | ||
ff, err := c.getAllFeatures(workspaceID, c.defaultTraits, false) | ||
if err != nil { | ||
return false, err | ||
} | ||
featureval, ok := ff[feature] | ||
if !ok { | ||
return false, newFeatureError(fmt.Sprintf("feature %s does not exist", feature)) | ||
} | ||
return featureval.Enabled, nil | ||
} | ||
|
||
// IsFeatureEnabledLatest checks if a feature is enabled for a workspace, bypassing the cache | ||
// Note: This method always fetches fresh values from the provider(bypassing the cache), which may impact performance. | ||
func (c *clientImpl) IsFeatureEnabledLatest(workspaceID string, feature string) (bool, error) { | ||
ff, err := c.getAllFeatures(workspaceID, c.defaultTraits, true) | ||
if err != nil { | ||
return false, err | ||
} | ||
return ff[feature].Enabled, nil | ||
} | ||
|
||
// GetFeatureValue gets the value of a feature for a workspace | ||
// Note: Result may be stale if returned from cache. Use GetFeatureValueLatest if stale values are not acceptable. | ||
func (c *clientImpl) GetFeatureValue(workspaceID string, feature string) (provider.FeatureValue, error) { | ||
ff, err := c.getAllFeatures(workspaceID, c.defaultTraits, false) | ||
if err != nil { | ||
return provider.FeatureValue{}, err | ||
} | ||
// create a copy of the feature value and return it | ||
featureval := *ff[feature] | ||
return featureval, nil | ||
} | ||
|
||
// GetFeatureValueLatest gets the value of a feature for a workspace, bypassing the cache | ||
// Note: This method always fetches fresh values from the provider(bypassing the cache), which may impact performance. | ||
func (c *clientImpl) GetFeatureValueLatest(workspaceID string, feature string) (provider.FeatureValue, error) { | ||
ff, err := c.getAllFeatures(workspaceID, c.defaultTraits, true) | ||
if err != nil { | ||
return provider.FeatureValue{}, err | ||
} | ||
// create a copy of the feature value and return it | ||
featureval := *ff[feature] | ||
return featureval, nil | ||
} | ||
|
||
// SetDefaultTraits sets the default traits for the feature flag client | ||
// These traits will always be used when fetching feature flags | ||
// This function is only meant to be called once in the application lifecycle. | ||
func (c *clientImpl) SetDefaultTraits(traits map[string]string) { | ||
c.defaultTraits = traits | ||
} | ||
|
||
func (c *clientImpl) getAllFeatures(workspaceID string, traits map[string]string, skipCache bool) (map[string]*provider.FeatureValue, error) { | ||
if !skipCache && c.cache.IsEnabled() { | ||
if val, ok := c.cache.Get(workspaceID); ok { | ||
if val.IsStale { | ||
// refresh the feature flags asynchronously if the cache is stale | ||
go c.refreshFeatureFlags(workspaceID) | ||
} | ||
return val.Value, nil | ||
} | ||
} | ||
|
||
// Fetch from provider, if cache is disabled or cache miss | ||
ff, err := c.provider.GetFeatureFlags(provider.ProviderParams{ | ||
WorkspaceID: workspaceID, | ||
Traits: traits, | ||
}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// Cache the fetched feature flags if cache is enabled | ||
if c.cache.IsEnabled() { | ||
if _, err := c.cache.Set(workspaceID, ff); err != nil { | ||
return nil, err | ||
} | ||
} | ||
return ff, nil | ||
} | ||
|
||
func (c *clientImpl) refreshFeatureFlags(workspaceID string) error { | ||
// fetch the feature flags from the provider | ||
ff, err := c.provider.GetFeatureFlags(provider.ProviderParams{ | ||
WorkspaceID: workspaceID, | ||
Traits: c.defaultTraits, | ||
}) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// set the feature flags in the cache | ||
if _, err := c.cache.Set(workspaceID, ff); err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} |
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 @@ | ||
package featureflags_test |
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,14 @@ | ||
package featureflags | ||
|
||
// featureError represents an error related to feature flag operations | ||
type featureError struct { | ||
Message string | ||
} | ||
|
||
func (e *featureError) Error() string { | ||
return e.Message | ||
} | ||
|
||
func newFeatureError(message string) *featureError { | ||
return &featureError{Message: message} | ||
} |
Oops, something went wrong.