Skip to content

Commit

Permalink
Merge pull request #402 from checkr/zz/refactor-eval-cache
Browse files Browse the repository at this point in the history
Refactor eval cache with atomic.Value
  • Loading branch information
zhouzhuojie authored Sep 16, 2020
2 parents 011e7cc + cedbe4d commit 2c35ad8
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 67 deletions.
17 changes: 7 additions & 10 deletions pkg/handler/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"math/rand"
"sync"
"time"

"github.com/checkr/flagr/pkg/config"
Expand Down Expand Up @@ -312,19 +313,15 @@ func debugConstraintMsg(enableDebug bool, expr conditions.Expr, m map[string]int
return fmt.Sprintf("constraint not match. constraint: %s, entity_context: %+v.", expr, m)
}

var rateLimitMap = make(map[uint]*ratelimit.RateLimiter)
var rateLimitMap = sync.Map{}

var rateLimitPerFlagConsoleLogging = func(r *models.EvalResult) {
flagID := util.SafeUint(r.FlagID)
rl, ok := rateLimitMap[flagID]
if !ok {
rl = ratelimit.New(
config.Config.RateLimiterPerFlagPerSecondConsoleLogging,
time.Second,
)
rateLimitMap[flagID] = rl
}
if !rl.Limit() {
rl, _ := rateLimitMap.LoadOrStore(flagID, ratelimit.New(
config.Config.RateLimiterPerFlagPerSecondConsoleLogging,
time.Second,
))
if !rl.(*ratelimit.RateLimiter).Limit() {
jsonStr, _ := json.Marshal(struct{ FlagEvalResult *models.EvalResult }{FlagEvalResult: r})
fmt.Println(string(jsonStr))
}
Expand Down
50 changes: 20 additions & 30 deletions pkg/handler/eval_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package handler

import (
"sync"
"sync/atomic"
"time"

"github.com/checkr/flagr/pkg/config"
Expand All @@ -17,16 +18,15 @@ var (
singletonEvalCacheOnce sync.Once
)

type mapCache map[string]*entity.Flag
type multiMapCache map[string]map[uint]*entity.Flag
type cacheContainer struct {
idCache map[string]*entity.Flag
keyCache map[string]*entity.Flag
tagCache map[string]map[uint]*entity.Flag
}

// EvalCache is the in-memory cache just for evaluation
type EvalCache struct {
mapCacheLock sync.RWMutex
idCache mapCache
keyCache mapCache
tagCache multiMapCache

cache atomic.Value
refreshTimeout time.Duration
refreshInterval time.Duration
}
Expand All @@ -35,9 +35,6 @@ type EvalCache struct {
var GetEvalCache = func() *EvalCache {
singletonEvalCacheOnce.Do(func() {
ec := &EvalCache{
idCache: make(map[string]*entity.Flag),
keyCache: make(map[string]*entity.Flag),
tagCache: make(map[string]map[uint]*entity.Flag),
refreshTimeout: config.Config.EvalCacheRefreshTimeout,
refreshInterval: config.Config.EvalCacheRefreshInterval,
}
Expand All @@ -63,22 +60,18 @@ func (ec *EvalCache) Start() {
}

func (ec *EvalCache) GetByTags(tags []string) []*entity.Flag {
ec.mapCacheLock.RLock()
defer ec.mapCacheLock.RUnlock()

results := map[uint]*entity.Flag{}
cache := ec.cache.Load().(*cacheContainer)
for _, t := range tags {
f, ok := ec.tagCache[t]
fSet, ok := cache.tagCache[t]
if ok {
for ia, va := range results {
f[ia] = va
for fID, f := range fSet {
results[fID] = f
}

results = f
}
}

values := []*entity.Flag{}
values := make([]*entity.Flag, 0, len(results))
for _, f := range results {
values = append(values, f)
}
Expand All @@ -88,13 +81,11 @@ func (ec *EvalCache) GetByTags(tags []string) []*entity.Flag {

// GetByFlagKeyOrID gets the flag by Key or ID
func (ec *EvalCache) GetByFlagKeyOrID(keyOrID interface{}) *entity.Flag {
ec.mapCacheLock.RLock()
defer ec.mapCacheLock.RUnlock()

s := util.SafeString(keyOrID)
f, ok := ec.idCache[s]
cache := ec.cache.Load().(*cacheContainer)
f, ok := cache.idCache[s]
if !ok {
f = ec.keyCache[s]
f = cache.keyCache[s]
}
return f
}
Expand All @@ -110,12 +101,11 @@ func (ec *EvalCache) reloadMapCache() error {
return nil, err
}

ec.mapCacheLock.Lock()
defer ec.mapCacheLock.Unlock()

ec.idCache = idCache
ec.keyCache = keyCache
ec.tagCache = tagCache
ec.cache.Store(&cacheContainer{
idCache: idCache,
keyCache: keyCache,
tagCache: tagCache,
})
return nil, err
})

Expand Down
11 changes: 4 additions & 7 deletions pkg/handler/eval_cache_fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,16 @@ type EvalCacheJSON struct {
}

func (ec *EvalCache) export() EvalCacheJSON {
fs := make([]entity.Flag, 0, len(ec.idCache))

ec.mapCacheLock.RLock()
defer ec.mapCacheLock.RUnlock()

for _, f := range ec.idCache {
idCache := ec.cache.Load().(*cacheContainer).idCache
fs := make([]entity.Flag, 0, len(idCache))
for _, f := range idCache {
ff := *f
fs = append(fs, ff)
}
return EvalCacheJSON{Flags: fs}
}

func (ec *EvalCache) fetchAllFlags() (idCache mapCache, keyCache mapCache, tagCache multiMapCache, err error) {
func (ec *EvalCache) fetchAllFlags() (idCache map[string]*entity.Flag, keyCache map[string]*entity.Flag, tagCache map[string]map[uint]*entity.Flag, err error) {
fs, err := fetchAllFlags()
if err != nil {
return nil, nil, nil, err
Expand Down
58 changes: 40 additions & 18 deletions pkg/handler/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ func TestEvalFlag(t *testing.T) {
FlagID: int64(100),
})
assert.NotNil(t, result)
assert.NotNil(t, result.VariantID)
assert.NotZero(t, result.VariantID)
})

t.Run("test happy code path with flagKey", func(t *testing.T) {
Expand All @@ -175,7 +175,7 @@ func TestEvalFlag(t *testing.T) {
FlagKey: "flag_key_100",
})
assert.NotNil(t, result)
assert.NotNil(t, result.VariantID)
assert.NotZero(t, result.VariantID)
})

t.Run("test happy code path with flagKey", func(t *testing.T) {
Expand All @@ -188,7 +188,7 @@ func TestEvalFlag(t *testing.T) {
FlagKey: "flag_key_100",
})
assert.NotNil(t, result)
assert.NotNil(t, result.VariantID)
assert.NotZero(t, result.VariantID)
})

t.Run("test happy code path with multiple constraints", func(t *testing.T) {
Expand Down Expand Up @@ -224,8 +224,9 @@ func TestEvalFlag(t *testing.T) {
},
}
f.PrepareEvaluation()
cache := &EvalCache{idCache: map[string]*entity.Flag{"100": &f}}
defer gostub.StubFunc(&GetEvalCache, cache).Reset()
ec := &EvalCache{}
ec.cache.Store(&cacheContainer{idCache: map[string]*entity.Flag{"100": &f}})
defer gostub.StubFunc(&GetEvalCache, ec).Reset()
result := EvalFlag(models.EvalContext{
EnableDebug: true,
EntityContext: map[string]interface{}{
Expand All @@ -238,7 +239,7 @@ func TestEvalFlag(t *testing.T) {
EntityType: "entityType1",
FlagID: int64(100),
})
assert.NotZero(t, result)
assert.NotNil(t, result)
assert.NotZero(t, result.VariantID)
})

Expand All @@ -249,8 +250,9 @@ func TestEvalFlag(t *testing.T) {
f.Segments[0].RolloutPercent = uint(0)

f.PrepareEvaluation()
cache := &EvalCache{idCache: map[string]*entity.Flag{"100": &f}}
defer gostub.StubFunc(&GetEvalCache, cache).Reset()
ec := &EvalCache{}
ec.cache.Store(&cacheContainer{idCache: map[string]*entity.Flag{"100": &f}})
defer gostub.StubFunc(&GetEvalCache, ec).Reset()
result := EvalFlag(models.EvalContext{
EnableDebug: true,
EntityContext: map[string]interface{}{"dl_state": "CA", "state": "CA", "rate": 2000},
Expand Down Expand Up @@ -281,8 +283,10 @@ func TestEvalFlag(t *testing.T) {
},
}
f.PrepareEvaluation()
cache := &EvalCache{idCache: map[string]*entity.Flag{"100": &f}}
defer gostub.StubFunc(&GetEvalCache, cache).Reset()

ec := &EvalCache{}
ec.cache.Store(&cacheContainer{idCache: map[string]*entity.Flag{"100": &f}})
defer gostub.StubFunc(&GetEvalCache, ec).Reset()
result := EvalFlag(models.EvalContext{
EnableDebug: true,
EntityContext: map[string]interface{}{"dl_state": "CA", "state": "NY"},
Expand All @@ -297,8 +301,9 @@ func TestEvalFlag(t *testing.T) {
t.Run("test enabled=false", func(t *testing.T) {
f := entity.GenFixtureFlag()
f.Enabled = false
cache := &EvalCache{idCache: map[string]*entity.Flag{"100": &f}}
defer gostub.StubFunc(&GetEvalCache, cache).Reset()
ec := &EvalCache{}
ec.cache.Store(&cacheContainer{idCache: map[string]*entity.Flag{"100": &f}})
defer gostub.StubFunc(&GetEvalCache, ec).Reset()
result := EvalFlag(models.EvalContext{
EnableDebug: true,
EntityContext: map[string]interface{}{"dl_state": "CA"},
Expand All @@ -314,8 +319,9 @@ func TestEvalFlag(t *testing.T) {
t.Run("empty entityType case", func(t *testing.T) {
f := entity.GenFixtureFlag()
f.EntityType = ""
cache := &EvalCache{idCache: map[string]*entity.Flag{"100": &f}}
defer gostub.StubFunc(&GetEvalCache, cache).Reset()
ec := &EvalCache{}
ec.cache.Store(&cacheContainer{idCache: map[string]*entity.Flag{"100": &f}})
defer gostub.StubFunc(&GetEvalCache, ec).Reset()
result := EvalFlag(models.EvalContext{
EnableDebug: true,
EntityContext: map[string]interface{}{"dl_state": "CA"},
Expand All @@ -324,14 +330,15 @@ func TestEvalFlag(t *testing.T) {
FlagID: int64(100),
})
assert.NotNil(t, result)
assert.NotNil(t, result.VariantID)
assert.NotZero(t, result.VariantID)
assert.Equal(t, "entityType1", result.EvalContext.EntityType)
})
t.Run("override case", func(t *testing.T) {
f := entity.GenFixtureFlag()
f.EntityType = "some_entity_type"
cache := &EvalCache{idCache: map[string]*entity.Flag{"100": &f}}
defer gostub.StubFunc(&GetEvalCache, cache).Reset()
ec := &EvalCache{}
ec.cache.Store(&cacheContainer{idCache: map[string]*entity.Flag{"100": &f}})
defer gostub.StubFunc(&GetEvalCache, ec).Reset()
result := EvalFlag(models.EvalContext{
EnableDebug: true,
EntityContext: map[string]interface{}{"dl_state": "CA"},
Expand All @@ -340,13 +347,28 @@ func TestEvalFlag(t *testing.T) {
FlagID: int64(100),
})
assert.NotNil(t, result)
assert.NotNil(t, result.VariantID)
assert.NotZero(t, result.VariantID)
assert.NotEqual(t, "entityType1", result.EvalContext.EntityType)
assert.Equal(t, "some_entity_type", result.EvalContext.EntityType)
})
})
}

func TestEvalFlagsByTags(t *testing.T) {
defer gostub.StubFunc(&logEvalResult).Reset()

t.Run("test happy code path", func(t *testing.T) {
defer gostub.StubFunc(&GetEvalCache, GenFixtureEvalCache()).Reset()
results := EvalFlagsByTags(models.EvalContext{
EnableDebug: true,
EntityContext: map[string]interface{}{"dl_state": "CA"},
FlagTags: []string{"tag1", "tag2"},
})
assert.NotZero(t, len(results))
assert.NotZero(t, results[0].VariantID)
})
}

func TestPostEvaluation(t *testing.T) {
t.Run("test empty body", func(t *testing.T) {
defer gostub.StubFunc(&EvalFlag, &models.EvalResult{}).Reset()
Expand Down
7 changes: 5 additions & 2 deletions pkg/handler/fixture.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ func GenFixtureEvalCache() *EvalCache {
tagCache[tag.Value] = map[uint]*entity.Flag{f.ID: &f}
}

return &EvalCache{
ec := &EvalCache{}
ec.cache.Store(&cacheContainer{
idCache: map[string]*entity.Flag{util.SafeString(f.ID): &f},
keyCache: map[string]*entity.Flag{f.Key: &f},
tagCache: tagCache,
}
})

return ec
}

0 comments on commit 2c35ad8

Please sign in to comment.