Skip to content

Commit

Permalink
feat: enable local evaluation (#129)
Browse files Browse the repository at this point in the history
Signed-off-by: Alessandro Yuichi Okimoto <[email protected]>
  • Loading branch information
cre8ivejp authored Jun 13, 2024
1 parent cf716a2 commit ea8092d
Show file tree
Hide file tree
Showing 23 changed files with 2,166 additions and 86 deletions.
4 changes: 3 additions & 1 deletion example/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ toolchain go1.21.10
require github.com/bucketeer-io/go-server-sdk v0.0.0-00010101000000-000000000000

require (
github.com/bucketeer-io/bucketeer/proto v0.0.0-20240520044049-69f55ef9a09f // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/bucketeer-io/bucketeer v0.4.4 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/exp v0.0.0-20240529005216-23cca8864a10 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
Expand Down
13 changes: 10 additions & 3 deletions example/go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/bucketeer-io/bucketeer/proto v0.0.0-20240520044049-69f55ef9a09f h1:X9kVAc7w3kOFez2Z4c3YZ7Hge4YERFrByOkWCaTJbsQ=
github.com/bucketeer-io/bucketeer/proto v0.0.0-20240520044049-69f55ef9a09f/go.mod h1:6v7NCnK4d26hyQ+ugRWB09PtoGnSD80AaW0QgdxwzD8=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/bucketeer-io/bucketeer v0.4.4 h1:2pOLWzclbSvR8OFwjue5dzXGU0g41qx7+7BuVh+OMGs=
github.com/bucketeer-io/bucketeer v0.4.4/go.mod h1:/mntqyp2jg65iAuVhFPV+zE7I8aR4M4AA/igZtGkwNA=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
Expand All @@ -13,8 +15,9 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
Expand All @@ -25,6 +28,8 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
Expand Down Expand Up @@ -52,6 +57,8 @@ go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240529005216-23cca8864a10 h1:vpzMC/iZhYFAjJzHU0Cfuq+w1vLLsF2vLkDrPjzKYck=
golang.org/x/exp v0.0.0-20240529005216-23cca8864a10/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ require (
)

require (
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/exp v0.0.0-20240529005216-23cca8864a10 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/bucketeer-io/bucketeer v0.4.4 h1:2pOLWzclbSvR8OFwjue5dzXGU0g41qx7+7BuVh+OMGs=
github.com/bucketeer-io/bucketeer v0.4.4/go.mod h1:/mntqyp2jg65iAuVhFPV+zE7I8aR4M4AA/igZtGkwNA=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
Expand All @@ -26,6 +28,8 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
Expand Down Expand Up @@ -53,6 +57,8 @@ go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240529005216-23cca8864a10 h1:vpzMC/iZhYFAjJzHU0Cfuq+w1vLLsF2vLkDrPjzKYck=
golang.org/x/exp v0.0.0-20240529005216-23cca8864a10/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
Expand Down
2 changes: 1 addition & 1 deletion pkg/bucketeer/cache/processor/feature_flag_processor.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:generate mockgen -source=$GOFILE -package=$GOPACKAGE -destination=../../../test/mock/$GOPACKAGE/$GOFILE
//go:generate mockgen -source=$GOFILE -package=$GOPACKAGE -destination=../../../../test/mock/$GOPACKAGE/$GOFILE
package processor

import (
Expand Down
2 changes: 1 addition & 1 deletion pkg/bucketeer/cache/processor/segment_user_processor.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:generate mockgen -source=$GOFILE -package=$GOPACKAGE -destination=../../../test/mock/$GOPACKAGE/$GOFILE
//go:generate mockgen -source=$GOFILE -package=$GOPACKAGE -destination=../../../../test/mock/$GOPACKAGE/$GOFILE
package processor

import (
Expand Down
141 changes: 141 additions & 0 deletions pkg/bucketeer/evaluator/evaluator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package evaluator

//go:generate mockgen -source=$GOFILE -package=$GOPACKAGE -destination=../../../test/mock/$GOPACKAGE/$GOFILE
import (
"errors"

evaluation "github.com/bucketeer-io/bucketeer/evaluation"
ftproto "github.com/bucketeer-io/bucketeer/proto/feature"
userproto "github.com/bucketeer-io/bucketeer/proto/user"

"github.com/bucketeer-io/go-server-sdk/pkg/bucketeer/cache"
"github.com/bucketeer-io/go-server-sdk/pkg/bucketeer/model"
"github.com/bucketeer-io/go-server-sdk/pkg/bucketeer/user"
)

type EvaluateLocally interface {
Evaluate(user *user.User, featureID string) (*model.Evaluation, error)
}

type evaluator struct {
tag string
featuresCache cache.FeaturesCache
segmentUsersCache cache.SegmentUsersCache
}

func NewEvaluator(
tag string,
featuresCache cache.FeaturesCache,
segmentUsersCache cache.SegmentUsersCache,
) EvaluateLocally {
return &evaluator{
tag: tag,
featuresCache: featuresCache,
segmentUsersCache: segmentUsersCache,
}
}

var (
errEvaluationNotFound = errors.New("evaluation not found")
)

func (e *evaluator) Evaluate(user *user.User, featureID string) (*model.Evaluation, error) {
// Get the target feature
feature, err := e.featuresCache.Get(featureID)
if err != nil {
return nil, err
}
// Include the prerequisite features if is configured
targetFeatures, err := e.getTargetFeatures(feature)
if err != nil {
return nil, err
}
// List and get all the segments if is configured in the targeting rules
evaluator := evaluation.NewEvaluator()
sIDs := evaluator.ListSegmentIDs(feature)
segments := make(map[string][]*ftproto.SegmentUser, len(sIDs))
for _, id := range sIDs {
segment, err := e.segmentUsersCache.Get(id)
if err != nil {
return nil, err
}
segments[segment.SegmentId] = segment.Users
}
// Convert to evaluation's user proto message
u := &userproto.User{
Id: user.ID,
Data: user.Data,
}
// Evaluate the end user
userEvaluations, err := evaluator.EvaluateFeatures(
targetFeatures,
u,
segments,
e.tag,
)
if err != nil {
return nil, err
}
// Find the target evaluation from the results
eval, err := e.findEvaluation(userEvaluations.Evaluations, featureID)
if err != nil {
return nil, err
}
// Convert to SDK's model
evaluation := &model.Evaluation{
ID: eval.Id,
FeatureID: eval.FeatureId,
FeatureVersion: eval.FeatureVersion,
UserID: eval.UserId,
VariationID: eval.VariationId,
VariationValue: eval.VariationValue,
Reason: &model.Reason{
Type: model.ReasonType(eval.Reason.Type.String()),
RuleID: eval.Reason.RuleId,
},
}
return evaluation, nil
}

func (e *evaluator) getTargetFeatures(feature *ftproto.Feature) ([]*ftproto.Feature, error) {
if len(feature.Prerequisites) == 0 {
return []*ftproto.Feature{feature}, nil
}
preFeatures, err := e.getPrerequisiteFeatures(feature)
if err != nil {
return nil, err
}
return append([]*ftproto.Feature{feature}, preFeatures...), nil
}

// Gets the features specified as prerequisite
func (e *evaluator) getPrerequisiteFeatures(feature *ftproto.Feature) ([]*ftproto.Feature, error) {
prerequisites := make(map[string]*ftproto.Feature)
queue := append([]*ftproto.Feature{}, feature)
for len(queue) > 0 {
f := queue[0]
for _, p := range f.Prerequisites {
preFeature, err := e.featuresCache.Get(p.FeatureId)
if err != nil {
return nil, err
}
prerequisites[preFeature.Id] = preFeature
queue = append(queue, preFeature)
}
queue = queue[1:]
}
ftList := make([]*ftproto.Feature, 0, len(prerequisites))
for _, v := range prerequisites {
ftList = append(ftList, v)
}
return ftList, nil
}

func (e *evaluator) findEvaluation(evals []*ftproto.Evaluation, id string) (*ftproto.Evaluation, error) {
for _, e := range evals {
if e.FeatureId == id {
return e, nil
}
}
return nil, errEvaluationNotFound
}
Loading

0 comments on commit ea8092d

Please sign in to comment.