Skip to content

Commit 283c838

Browse files
yasirfolio3Michael Ng
authored andcommitted
feature (multi-rollout): Added support for multiple rollouts. (#247)
feature (multi-rollout): Added support for multiple rollouts
1 parent 2718152 commit 283c838

File tree

3 files changed

+230
-54
lines changed

3 files changed

+230
-54
lines changed

pkg/decision/helpers_test.go

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************************
2-
* Copyright 2019, Optimizely, Inc. and contributors *
2+
* Copyright 2019-2020, Optimizely, Inc. and contributors *
33
* *
44
* Licensed under the Apache License, Version 2.0 (the "License"); *
55
* you may not use this file except in compliance with the License. *
@@ -150,6 +150,48 @@ var testExp1112 = entities.Experiment{
150150
entities.Range{EntityID: "2222", EndOfRange: 10000},
151151
},
152152
}
153+
var testExp1117Var2223 = entities.Variation{ID: "2223", Key: "2223"}
154+
var testAudience5556 = entities.Audience{ID: "5556"}
155+
var testExp1117 = entities.Experiment{
156+
AudienceConditionTree: &entities.TreeNode{
157+
Operator: "and",
158+
Nodes: []*entities.TreeNode{
159+
&entities.TreeNode{Item: "test_audience_5556"},
160+
},
161+
},
162+
ID: "1117",
163+
Key: testExp1111Key,
164+
Variations: map[string]entities.Variation{
165+
"2223": testExp1117Var2223,
166+
},
167+
VariationKeyToIDMap: map[string]string{
168+
"2223": "2223",
169+
},
170+
TrafficAllocation: []entities.Range{
171+
entities.Range{EntityID: "2223", EndOfRange: 10000},
172+
},
173+
}
174+
var testExp1118Var2224 = entities.Variation{ID: "2224", Key: "2224"}
175+
var testAudience5557 = entities.Audience{ID: "5557"}
176+
var testExp1118 = entities.Experiment{
177+
AudienceConditionTree: &entities.TreeNode{
178+
Operator: "and",
179+
Nodes: []*entities.TreeNode{
180+
&entities.TreeNode{Item: "test_audience_5557"},
181+
},
182+
},
183+
ID: "1118",
184+
Key: testExp1111Key,
185+
Variations: map[string]entities.Variation{
186+
"2224": testExp1118Var2224,
187+
},
188+
VariationKeyToIDMap: map[string]string{
189+
"2224": "2224",
190+
},
191+
TrafficAllocation: []entities.Range{
192+
entities.Range{EntityID: "2224", EndOfRange: 10000},
193+
},
194+
}
153195

154196
const testFeatRollout3334Key = "test_feature_rollout_3334_key"
155197

@@ -158,7 +200,7 @@ var testFeatRollout3334 = entities.Feature{
158200
Key: testFeatRollout3334Key,
159201
Rollout: entities.Rollout{
160202
ID: "4444",
161-
Experiments: []entities.Experiment{testExp1112},
203+
Experiments: []entities.Experiment{testExp1112, testExp1117, testExp1118},
162204
},
163205
}
164206

pkg/decision/rollout_service.go

Lines changed: 58 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -30,25 +30,61 @@ import (
3030
type RolloutService struct {
3131
audienceTreeEvaluator evaluator.TreeEvaluator
3232
experimentBucketerService ExperimentService
33-
logger logging.OptimizelyLogProducer
33+
logger logging.OptimizelyLogProducer
3434
}
3535

3636
// NewRolloutService returns a new instance of the Rollout service
3737
func NewRolloutService(sdkKey string) *RolloutService {
3838
return &RolloutService{
39-
logger:logging.GetLogger(sdkKey, "RolloutService"),
39+
logger: logging.GetLogger(sdkKey, "RolloutService"),
4040
audienceTreeEvaluator: evaluator.NewMixedTreeEvaluator(),
4141
experimentBucketerService: NewExperimentBucketerService(logging.GetLogger(sdkKey, "ExperimentBucketerService")),
4242
}
4343
}
4444

4545
// GetDecision returns a decision for the given feature and user context
4646
func (r RolloutService) GetDecision(decisionContext FeatureDecisionContext, userContext entities.UserContext) (FeatureDecision, error) {
47+
4748
featureDecision := FeatureDecision{
4849
Source: Rollout,
4950
}
5051
feature := decisionContext.Feature
5152
rollout := feature.Rollout
53+
54+
evaluateConditionTree := func(experiment *entities.Experiment) bool {
55+
condTreeParams := entities.NewTreeParameters(&userContext, decisionContext.ProjectConfig.GetAudienceMap())
56+
evalResult, _ := r.audienceTreeEvaluator.Evaluate(experiment.AudienceConditionTree, condTreeParams)
57+
if !evalResult {
58+
featureDecision.Reason = reasons.FailedRolloutTargeting
59+
r.logger.Debug(fmt.Sprintf(`User "%s" failed targeting for feature rollout with key "%s".`, userContext.ID, feature.Key))
60+
}
61+
return evalResult
62+
}
63+
64+
getFeatureDecision := func(experiment *entities.Experiment, decision *ExperimentDecision) (FeatureDecision, error) {
65+
// translate the experiment reason into a more rollouts-appropriate reason
66+
switch decision.Reason {
67+
case reasons.NotBucketedIntoVariation:
68+
featureDecision.Decision = Decision{Reason: reasons.FailedRolloutBucketing}
69+
case reasons.BucketedIntoVariation:
70+
featureDecision.Decision = Decision{Reason: reasons.BucketedIntoRollout}
71+
default:
72+
featureDecision.Decision = decision.Decision
73+
}
74+
75+
featureDecision.Experiment = *experiment
76+
featureDecision.Variation = decision.Variation
77+
r.logger.Debug(fmt.Sprintf(`Decision made for user "%s" for feature rollout with key "%s": %s.`, userContext.ID, feature.Key, featureDecision.Reason))
78+
return featureDecision, nil
79+
}
80+
81+
getExperimentDecisionContext := func(experiment *entities.Experiment) ExperimentDecisionContext {
82+
return ExperimentDecisionContext{
83+
Experiment: experiment,
84+
ProjectConfig: decisionContext.ProjectConfig,
85+
}
86+
}
87+
5288
if rollout.ID == "" {
5389
featureDecision.Reason = reasons.NoRolloutForFeature
5490
return featureDecision, nil
@@ -60,38 +96,30 @@ func (r RolloutService) GetDecision(decisionContext FeatureDecisionContext, user
6096
return featureDecision, nil
6197
}
6298

63-
// For now, Rollouts is just a single experiment layer
64-
experiment := rollout.Experiments[0]
65-
experimentDecisionContext := ExperimentDecisionContext{
66-
Experiment: &experiment,
67-
ProjectConfig: decisionContext.ProjectConfig,
68-
}
69-
70-
// if user fails rollout targeting rule we return out of it
71-
if experiment.AudienceConditionTree != nil {
72-
condTreeParams := entities.NewTreeParameters(&userContext, decisionContext.ProjectConfig.GetAudienceMap())
73-
evalResult, _ := r.audienceTreeEvaluator.Evaluate(experiment.AudienceConditionTree, condTreeParams)
74-
if !evalResult {
75-
featureDecision.Reason = reasons.FailedRolloutTargeting
76-
r.logger.Debug(fmt.Sprintf(`User "%s" failed targeting for feature rollout with key "%s".`, userContext.ID, feature.Key))
77-
return featureDecision, nil
99+
for index := 0; index < numberOfExperiments-1; index++ {
100+
experiment := &rollout.Experiments[index]
101+
experimentDecisionContext := getExperimentDecisionContext(experiment)
102+
// Move to next evaluation if condition tree is available and evaluation fails
103+
if experiment.AudienceConditionTree != nil && !evaluateConditionTree(experiment) {
104+
// Evaluate this user for the next rule
105+
continue
106+
}
107+
decision, _ := r.experimentBucketerService.GetDecision(experimentDecisionContext, userContext)
108+
if decision.Variation == nil {
109+
// Evaluate fall back rule / last rule now
110+
break
78111
}
112+
return getFeatureDecision(experiment, &decision)
79113
}
80114

81-
decision, _ := r.experimentBucketerService.GetDecision(experimentDecisionContext, userContext)
82-
// translate the experiment reason into a more rollouts-appropriate reason
83-
switch decision.Reason {
84-
case reasons.NotBucketedIntoVariation:
85-
featureDecision.Decision = Decision{Reason: reasons.FailedRolloutBucketing}
86-
case reasons.BucketedIntoVariation:
87-
featureDecision.Decision = Decision{Reason: reasons.BucketedIntoRollout}
88-
default:
89-
featureDecision.Decision = decision.Decision
115+
// fall back rule / last rule
116+
experiment := &rollout.Experiments[numberOfExperiments-1]
117+
experimentDecisionContext := getExperimentDecisionContext(experiment)
118+
// Move to bucketing if conditionTree is unavailable or evaluation passes
119+
if experiment.AudienceConditionTree == nil || evaluateConditionTree(experiment) {
120+
decision, _ := r.experimentBucketerService.GetDecision(experimentDecisionContext, userContext)
121+
return getFeatureDecision(experiment, &decision)
90122
}
91123

92-
featureDecision.Experiment = experiment
93-
featureDecision.Variation = decision.Variation
94-
r.logger.Debug(fmt.Sprintf(`Decision made for user "%s" for feature rollout with key "%s": %s.`, userContext.ID, feature.Key, featureDecision.Reason))
95-
96124
return featureDecision, nil
97125
}

0 commit comments

Comments
 (0)