Skip to content

Commit

Permalink
feature (multi-rollout): Added support for multiple rollouts. (#247)
Browse files Browse the repository at this point in the history
feature (multi-rollout): Added support for multiple rollouts
  • Loading branch information
yasirfolio3 authored Apr 1, 2020
1 parent 8fc5386 commit 3ae7201
Show file tree
Hide file tree
Showing 3 changed files with 230 additions and 54 deletions.
46 changes: 44 additions & 2 deletions pkg/decision/helpers_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/****************************************************************************
* Copyright 2019, Optimizely, Inc. and contributors *
* Copyright 2019-2020, Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
Expand Down Expand Up @@ -150,6 +150,48 @@ var testExp1112 = entities.Experiment{
entities.Range{EntityID: "2222", EndOfRange: 10000},
},
}
var testExp1117Var2223 = entities.Variation{ID: "2223", Key: "2223"}
var testAudience5556 = entities.Audience{ID: "5556"}
var testExp1117 = entities.Experiment{
AudienceConditionTree: &entities.TreeNode{
Operator: "and",
Nodes: []*entities.TreeNode{
&entities.TreeNode{Item: "test_audience_5556"},
},
},
ID: "1117",
Key: testExp1111Key,
Variations: map[string]entities.Variation{
"2223": testExp1117Var2223,
},
VariationKeyToIDMap: map[string]string{
"2223": "2223",
},
TrafficAllocation: []entities.Range{
entities.Range{EntityID: "2223", EndOfRange: 10000},
},
}
var testExp1118Var2224 = entities.Variation{ID: "2224", Key: "2224"}
var testAudience5557 = entities.Audience{ID: "5557"}
var testExp1118 = entities.Experiment{
AudienceConditionTree: &entities.TreeNode{
Operator: "and",
Nodes: []*entities.TreeNode{
&entities.TreeNode{Item: "test_audience_5557"},
},
},
ID: "1118",
Key: testExp1111Key,
Variations: map[string]entities.Variation{
"2224": testExp1118Var2224,
},
VariationKeyToIDMap: map[string]string{
"2224": "2224",
},
TrafficAllocation: []entities.Range{
entities.Range{EntityID: "2224", EndOfRange: 10000},
},
}

const testFeatRollout3334Key = "test_feature_rollout_3334_key"

Expand All @@ -158,7 +200,7 @@ var testFeatRollout3334 = entities.Feature{
Key: testFeatRollout3334Key,
Rollout: entities.Rollout{
ID: "4444",
Experiments: []entities.Experiment{testExp1112},
Experiments: []entities.Experiment{testExp1112, testExp1117, testExp1118},
},
}

Expand Down
88 changes: 58 additions & 30 deletions pkg/decision/rollout_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,61 @@ import (
type RolloutService struct {
audienceTreeEvaluator evaluator.TreeEvaluator
experimentBucketerService ExperimentService
logger logging.OptimizelyLogProducer
logger logging.OptimizelyLogProducer
}

// NewRolloutService returns a new instance of the Rollout service
func NewRolloutService(sdkKey string) *RolloutService {
return &RolloutService{
logger:logging.GetLogger(sdkKey, "RolloutService"),
logger: logging.GetLogger(sdkKey, "RolloutService"),
audienceTreeEvaluator: evaluator.NewMixedTreeEvaluator(),
experimentBucketerService: NewExperimentBucketerService(logging.GetLogger(sdkKey, "ExperimentBucketerService")),
}
}

// GetDecision returns a decision for the given feature and user context
func (r RolloutService) GetDecision(decisionContext FeatureDecisionContext, userContext entities.UserContext) (FeatureDecision, error) {

featureDecision := FeatureDecision{
Source: Rollout,
}
feature := decisionContext.Feature
rollout := feature.Rollout

evaluateConditionTree := func(experiment *entities.Experiment) bool {
condTreeParams := entities.NewTreeParameters(&userContext, decisionContext.ProjectConfig.GetAudienceMap())
evalResult, _ := r.audienceTreeEvaluator.Evaluate(experiment.AudienceConditionTree, condTreeParams)
if !evalResult {
featureDecision.Reason = reasons.FailedRolloutTargeting
r.logger.Debug(fmt.Sprintf(`User "%s" failed targeting for feature rollout with key "%s".`, userContext.ID, feature.Key))
}
return evalResult
}

getFeatureDecision := func(experiment *entities.Experiment, decision *ExperimentDecision) (FeatureDecision, error) {
// translate the experiment reason into a more rollouts-appropriate reason
switch decision.Reason {
case reasons.NotBucketedIntoVariation:
featureDecision.Decision = Decision{Reason: reasons.FailedRolloutBucketing}
case reasons.BucketedIntoVariation:
featureDecision.Decision = Decision{Reason: reasons.BucketedIntoRollout}
default:
featureDecision.Decision = decision.Decision
}

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

getExperimentDecisionContext := func(experiment *entities.Experiment) ExperimentDecisionContext {
return ExperimentDecisionContext{
Experiment: experiment,
ProjectConfig: decisionContext.ProjectConfig,
}
}

if rollout.ID == "" {
featureDecision.Reason = reasons.NoRolloutForFeature
return featureDecision, nil
Expand All @@ -60,38 +96,30 @@ func (r RolloutService) GetDecision(decisionContext FeatureDecisionContext, user
return featureDecision, nil
}

// For now, Rollouts is just a single experiment layer
experiment := rollout.Experiments[0]
experimentDecisionContext := ExperimentDecisionContext{
Experiment: &experiment,
ProjectConfig: decisionContext.ProjectConfig,
}

// if user fails rollout targeting rule we return out of it
if experiment.AudienceConditionTree != nil {
condTreeParams := entities.NewTreeParameters(&userContext, decisionContext.ProjectConfig.GetAudienceMap())
evalResult, _ := r.audienceTreeEvaluator.Evaluate(experiment.AudienceConditionTree, condTreeParams)
if !evalResult {
featureDecision.Reason = reasons.FailedRolloutTargeting
r.logger.Debug(fmt.Sprintf(`User "%s" failed targeting for feature rollout with key "%s".`, userContext.ID, feature.Key))
return featureDecision, nil
for index := 0; index < numberOfExperiments-1; index++ {
experiment := &rollout.Experiments[index]
experimentDecisionContext := getExperimentDecisionContext(experiment)
// Move to next evaluation if condition tree is available and evaluation fails
if experiment.AudienceConditionTree != nil && !evaluateConditionTree(experiment) {
// Evaluate this user for the next rule
continue
}
decision, _ := r.experimentBucketerService.GetDecision(experimentDecisionContext, userContext)
if decision.Variation == nil {
// Evaluate fall back rule / last rule now
break
}
return getFeatureDecision(experiment, &decision)
}

decision, _ := r.experimentBucketerService.GetDecision(experimentDecisionContext, userContext)
// translate the experiment reason into a more rollouts-appropriate reason
switch decision.Reason {
case reasons.NotBucketedIntoVariation:
featureDecision.Decision = Decision{Reason: reasons.FailedRolloutBucketing}
case reasons.BucketedIntoVariation:
featureDecision.Decision = Decision{Reason: reasons.BucketedIntoRollout}
default:
featureDecision.Decision = decision.Decision
// fall back rule / last rule
experiment := &rollout.Experiments[numberOfExperiments-1]
experimentDecisionContext := getExperimentDecisionContext(experiment)
// Move to bucketing if conditionTree is unavailable or evaluation passes
if experiment.AudienceConditionTree == nil || evaluateConditionTree(experiment) {
decision, _ := r.experimentBucketerService.GetDecision(experimentDecisionContext, userContext)
return getFeatureDecision(experiment, &decision)
}

featureDecision.Experiment = experiment
featureDecision.Variation = decision.Variation
r.logger.Debug(fmt.Sprintf(`Decision made for user "%s" for feature rollout with key "%s": %s.`, userContext.ID, feature.Key, featureDecision.Reason))

return featureDecision, nil
}
Loading

0 comments on commit 3ae7201

Please sign in to comment.