Skip to content

Commit

Permalink
[SDKS-7679] Implement getTreatments and create FlagSetsFilter in factory
Browse files Browse the repository at this point in the history
  • Loading branch information
nmayorsplit committed Nov 14, 2023
1 parent 821e686 commit 6556fcc
Show file tree
Hide file tree
Showing 9 changed files with 327 additions and 41 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/splitio/go-client/v6
go 1.18

require (
github.com/splitio/go-split-commons/v5 v5.0.1-0.20231107180451-bc39808d0f93
github.com/splitio/go-split-commons/v5 v5.0.1-0.20231114140907-c89079357dca
github.com/splitio/go-toolkit/v5 v5.3.2-0.20231106173125-49e72b9823dc
)

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/redis/go-redis/v9 v9.0.4 h1:FC82T+CHJ/Q/PdyLW++GeCO+Ol59Y4T7R4jbgjvktgc=
github.com/redis/go-redis/v9 v9.0.4/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
github.com/splitio/go-split-commons/v5 v5.0.1-0.20231107180451-bc39808d0f93 h1:E/91apLmtONP5lVTBJJbBZUk/RNn7C2J7Z/8Q2yQo6s=
github.com/splitio/go-split-commons/v5 v5.0.1-0.20231107180451-bc39808d0f93/go.mod h1:PSkBLDXQW7NAhZ7JO1va7QJyTeDvpE7MEDnTdn5evRM=
github.com/splitio/go-split-commons/v5 v5.0.1-0.20231114140907-c89079357dca h1:fWhN3KBkAO4FUFcvVX87MdShbiynf6IlgddAunuZul4=
github.com/splitio/go-split-commons/v5 v5.0.1-0.20231114140907-c89079357dca/go.mod h1:PSkBLDXQW7NAhZ7JO1va7QJyTeDvpE7MEDnTdn5evRM=
github.com/splitio/go-toolkit/v5 v5.3.2-0.20231106173125-49e72b9823dc h1:14jdJE/rBEYfs1CO8kOQrj/8azszRFU4yw5FQIGpoJg=
github.com/splitio/go-toolkit/v5 v5.3.2-0.20231106173125-49e72b9823dc/go.mod h1:xYhUvV1gga9/1029Wbp5pjnR6Cy8nvBpjw99wAbsMko=
github.com/twmb/murmur3 v1.1.6 h1:mqrRot1BRxm+Yct+vavLMou2/iJt0tNVTTC0QoIjaZg=
Expand Down
132 changes: 105 additions & 27 deletions splitio/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,28 @@ func (c *SplitClient) generateControlTreatments(featureFlagNames []string, opera
return treatments
}

func (c *SplitClient) processResult(result evaluator.Results, operation string, bucketingKey *string, matchingKey string, attributes map[string]interface{}, metricsLabel string) (t map[string]TreatmentResult) {
var bulkImpressions []dtos.Impression
treatments := make(map[string]TreatmentResult)
for feature, evaluation := range result.Evaluations {
if !c.validator.IsSplitFound(evaluation.Label, feature, operation) {
treatments[feature] = TreatmentResult{
Treatment: evaluator.Control,
Config: nil,
}
} else {
bulkImpressions = append(bulkImpressions, c.createImpression(feature, bucketingKey, evaluation.Label, matchingKey, evaluation.Treatment, evaluation.SplitChangeNumber))

treatments[feature] = TreatmentResult{
Treatment: evaluation.Treatment,
Config: evaluation.Config,
}
}
}
c.storeData(bulkImpressions, attributes, metricsLabel, result.EvaluationTime)
return treatments
}

// doTreatmentsCall retrieves treatments of an specific array of feature flag names with configurations object if it is present for a certain key and set of attributes
func (c *SplitClient) doTreatmentsCall(key interface{}, featureFlagNames []string, attributes map[string]interface{}, operation string, metricsLabel string) (t map[string]TreatmentResult) {
treatments := make(map[string]TreatmentResult)
Expand Down Expand Up @@ -248,26 +270,44 @@ func (c *SplitClient) doTreatmentsCall(key interface{}, featureFlagNames []strin
return map[string]TreatmentResult{}
}

var bulkImpressions []dtos.Impression
evaluationsResult := c.getEvaluationsResult(matchingKey, bucketingKey, filteredFeatures, attributes, operation)
for feature, evaluation := range evaluationsResult.Evaluations {
if !c.validator.IsSplitFound(evaluation.Label, feature, operation) {
treatments[feature] = TreatmentResult{
Treatment: evaluator.Control,
Config: nil,
}
} else {
bulkImpressions = append(bulkImpressions, c.createImpression(feature, bucketingKey, evaluation.Label, matchingKey, evaluation.Treatment, evaluation.SplitChangeNumber))

treatments[feature] = TreatmentResult{
Treatment: evaluation.Treatment,
Config: evaluation.Config,
}
treatments = c.processResult(evaluationsResult, operation, bucketingKey, matchingKey, attributes, metricsLabel)

return treatments
}

// doTreatmentsCallByFlagSets retrieves treatments of a specific array of feature flag names, that belong to flag sets, with configurations object if it is present for a certain key and set of attributes
func (c *SplitClient) doTreatmentsCallByFlagSets(key interface{}, sets []string, attributes map[string]interface{}, operation string, metricsLabel string) (t map[string]TreatmentResult) {
treatments := make(map[string]TreatmentResult)

// Set up a guard deferred function to recover if the SDK starts panicking
defer func() {
if r := recover(); r != nil {
// At this point we'll only trust that the logger isn't panicking trust
// that the logger isn't panicking
c.evaluationTelemetry.RecordException(metricsLabel)
c.logger.Error(
"SDK is panicking with the following error", r, "\n",
string(debug.Stack()), "\n")
t = treatments
}
}()

if c.isDestroyed() {
return treatments
}

c.storeData(bulkImpressions, attributes, metricsLabel, evaluationsResult.EvaluationTime)
matchingKey, bucketingKey, err := c.validator.ValidateTreatmentKey(key, operation)
if err != nil {
c.logger.Error(err.Error())
return treatments
}

if c.isReady() {
evaluationsResult := c.evaluator.EvaluateFeatureByFlagSets(matchingKey, bucketingKey, sets, attributes)
treatments = c.processResult(evaluationsResult, operation, bucketingKey, matchingKey, attributes, metricsLabel)
}
return treatments
}

Expand All @@ -281,29 +321,47 @@ func (c *SplitClient) Treatments(key interface{}, featureFlagNames []string, att
return treatmentsResult
}

func (c *SplitClient) TreatmentsByFlagSet(key interface{}, set string, attributes map[string]interface{}) map[string]string {
return c.TreatmentsByFlagSets(key, []string{set}, attributes)
}

func (c *SplitClient) TreatmentsByFlagSets(key interface{}, sets []string, attributes map[string]interface{}) map[string]string {
treatmentsResult := map[string]string{}
func (c *SplitClient) validateSets(sets []string) []string {
if len(sets) == 0 {
c.logger.Warning("sets must be a non-empty array")
return treatmentsResult
return nil
}
sets, err := flagsets.SanitizeMany(sets)
if err != nil {
return treatmentsResult
return nil
}
sets = c.filterSetsAreInConfig(sets)
if len(sets) == 0 {
return nil
}
return sets
}

// Treatments evaluate multiple feature flag names belonging to a flag set for a single user and a set of attributes at once
func (c *SplitClient) TreatmentsByFlagSet(key interface{}, set string, attributes map[string]interface{}) map[string]string {
treatmentsResult := map[string]string{}
sets := c.validateSets([]string{set})
if sets == nil {
return treatmentsResult
}
result := c.doTreatmentsCallByFlagSets(key, sets, attributes, treatmentsByFlagSet, telemetry.TreatmentsByFlagSet)
for feature, treatmentResult := range result {
treatmentsResult[feature] = treatmentResult.Treatment
}
return treatmentsResult
}

// Treatments evaluate multiple feature flag names belonging to flag sets for a single user and a set of attributes at once
func (c *SplitClient) TreatmentsByFlagSets(key interface{}, sets []string, attributes map[string]interface{}) map[string]string {
treatmentsResult := map[string]string{}
sets = c.validateSets(sets)
if sets == nil {
return treatmentsResult
}
if !c.isReady()
// result := c.doTreatmentsCall(key, featureFlagNames, attributes, treatments, telemetry.Treatments)
// for feature, treatmentResult := range result {
// treatmentsResult[feature] = treatmentResult.Treatment
// }
result := c.doTreatmentsCallByFlagSets(key, sets, attributes, treatmentsByFlagSets, telemetry.TreatmentsByFlagSets)
for feature, treatmentResult := range result {
treatmentsResult[feature] = treatmentResult.Treatment
}
return treatmentsResult
}

Expand All @@ -324,6 +382,26 @@ func (c *SplitClient) TreatmentsWithConfig(key interface{}, featureFlagNames []s
return c.doTreatmentsCall(key, featureFlagNames, attributes, treatmentsWithConfig, telemetry.TreatmentsWithConfig)
}

// TreatmentsWithConfigByFlagSet evaluates multiple feature flag names belonging to a flag set for a single user and set of attributes at once and returns configurations
func (c *SplitClient) TreatmentsWithConfigByFlagSet(key interface{}, set string, attributes map[string]interface{}) map[string]TreatmentResult {
treatmentsResult := make(map[string]TreatmentResult)
sets := c.validateSets([]string{set})
if sets == nil {
return treatmentsResult
}
return c.doTreatmentsCallByFlagSets(key, sets, attributes, treatmentsWithConfigByFlagSet, telemetry.TreatmentsByFlagSets)
}

// TreatmentsWithConfigByFlagSet evaluates multiple feature flag names belonging to a flag sets for a single user and set of attributes at once and returns configurations
func (c *SplitClient) TreatmentsWithConfigByFlagSets(key interface{}, sets []string, attributes map[string]interface{}) map[string]TreatmentResult {
treatmentsResult := make(map[string]TreatmentResult)
sets = c.validateSets(sets)
if sets == nil {
return treatmentsResult
}
return c.doTreatmentsCallByFlagSets(key, sets, attributes, treatmentsWithConfigByFlagSets, telemetry.TreatmentsByFlagSets)
}

// isDestroyed returns true if the client has been destroyed
func (c *SplitClient) isDestroyed() bool {
return c.factory.IsDestroyed()
Expand Down
175 changes: 175 additions & 0 deletions splitio/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ import (

type mockEvaluator struct{}

// EvaluateFeatureByFlagSets implements evaluator.Interface.
func (*mockEvaluator) EvaluateFeatureByFlagSets(key string, bucketingKey *string, flagSets []string, attributes map[string]interface{}) evaluator.Results {
panic("unimplemented")
}

func (e *mockEvaluator) EvaluateFeature(
key string,
bucketingKey *string,
Expand Down Expand Up @@ -151,6 +156,32 @@ func getFactory() SplitFactory {
}
}

func getFactoryByFlagSets() SplitFactory {
telemetryStorage, _ := inmemory.NewTelemetryStorage()
cfg := conf.Default()
cfg.LabelsEnabled = true
cfg.Advanced.FlagSetFilter = []string{"set1", "set2"}
logger := logging.NewLogger(nil)

impressionObserver, _ := strategy.NewImpressionObserver(500)
impressionsCounter := strategy.NewImpressionsCounter()
impressionsStrategy := strategy.NewOptimizedImpl(impressionObserver, impressionsCounter, telemetryStorage, false)
impressionManager := provisional.NewImpressionManager(impressionsStrategy)

return SplitFactory{
cfg: cfg,
storages: sdkStorages{
impressions: mutexqueue.NewMQImpressionsStorage(cfg.Advanced.ImpressionsQueueSize, make(chan string, 1), logger, telemetryStorage),
events: mocks.MockEventStorage{},
initTelemetry: telemetryStorage,
runtimeTelemetry: telemetryStorage,
evaluationTelemetry: telemetryStorage,
},
impressionManager: impressionManager,
logger: logger,
}
}

func expectedTreatment(treatment string, expectedTreatment string, t *testing.T) {
if treatment != expectedTreatment {
t.Error("Expected: " + expectedTreatment + " actual: " + treatment)
Expand Down Expand Up @@ -197,6 +228,150 @@ func TestClientGetTreatment(t *testing.T) {
}
}

func TestClientGetTreatmentByFlagSet(t *testing.T) {
factory := getFactoryByFlagSets()
client := factory.Client()
client.evaluator = evaluatorMock.MockEvaluator{
EvaluateFeatureByFlagSetsCall: func(key string, bucketingKey *string, flagSets []string, attributes map[string]interface{}) evaluator.Results {
results := evaluator.Results{
Evaluations: make(map[string]evaluator.Result),
EvaluationTime: 0,
}
for _, flagSet := range flagSets {
switch flagSet {
case "set1":
results.Evaluations["feature"] = evaluator.Result{
EvaluationTime: 0,
Label: "aLabel",
SplitChangeNumber: 123,
Treatment: "TreatmentA",
}
default:
t.Error("Should be set1 or set2")
}
}
return results
},
}
factory.status.Store(sdkStatusReady)

res := client.TreatmentsByFlagSet("user1", "set1", nil)

expectedTreatment(res["feature"], "TreatmentA", t)
}

func TestClientGetTreatmentByFlagSets(t *testing.T) {
factory := getFactory()
client := factory.Client()
client.evaluator = evaluatorMock.MockEvaluator{
EvaluateFeatureByFlagSetsCall: func(key string, bucketingKey *string, flagSets []string, attributes map[string]interface{}) evaluator.Results {
results := evaluator.Results{
Evaluations: make(map[string]evaluator.Result),
EvaluationTime: 0,
}
for _, flagSet := range flagSets {
switch flagSet {
case "set1":
results.Evaluations["feature"] = evaluator.Result{
EvaluationTime: 0,
Label: "aLabel",
SplitChangeNumber: 123,
Treatment: "TreatmentA",
}
case "set2":
results.Evaluations["feature2"] = evaluator.Result{
EvaluationTime: 0,
Label: "bLabel",
SplitChangeNumber: 123,
Treatment: "TreatmentB",
}
default:
t.Error("Should be set1 or set2")
}
}
return results
},
}
factory.status.Store(sdkStatusReady)

res := client.TreatmentsByFlagSets("user1", []string{"set1", "set2"}, nil)

expectedTreatment(res["feature"], "TreatmentA", t)
expectedTreatment(res["feature2"], "TreatmentB", t)
}

func TestClientGetTreatmentWithConfigByFlagSet(t *testing.T) {
factory := getFactory()
client := factory.Client()
client.evaluator = evaluatorMock.MockEvaluator{
EvaluateFeatureByFlagSetsCall: func(key string, bucketingKey *string, flagSets []string, attributes map[string]interface{}) evaluator.Results {
results := evaluator.Results{
Evaluations: make(map[string]evaluator.Result),
EvaluationTime: 0,
}
for _, flagSet := range flagSets {
switch flagSet {
case "set1":
results.Evaluations["feature"] = evaluator.Result{
EvaluationTime: 0,
Label: "aLabel",
SplitChangeNumber: 123,
Treatment: "TreatmentA",
}
default:
t.Error("Should be set1 or set2")
}
}
return results
},
}
factory.status.Store(sdkStatusReady)

res := client.TreatmentsWithConfigByFlagSet("user1", "set1", nil)

expectedTreatment(res["feature"].Treatment, "TreatmentA", t)
}

func TestClientGetTreatmentWithConfigByFlagSets(t *testing.T) {
factory := getFactory()
client := factory.Client()
client.evaluator = evaluatorMock.MockEvaluator{
EvaluateFeatureByFlagSetsCall: func(key string, bucketingKey *string, flagSets []string, attributes map[string]interface{}) evaluator.Results {
results := evaluator.Results{
Evaluations: make(map[string]evaluator.Result),
EvaluationTime: 0,
}
for _, flagSet := range flagSets {
switch flagSet {
case "set1":
results.Evaluations["feature"] = evaluator.Result{
EvaluationTime: 0,
Label: "aLabel",
SplitChangeNumber: 123,
Treatment: "TreatmentA",
}
case "set2":
results.Evaluations["feature2"] = evaluator.Result{
EvaluationTime: 0,
Label: "bLabel",
SplitChangeNumber: 123,
Treatment: "TreatmentB",
}
default:
t.Error("Should be set1 or set2")
}
}
return results
},
}
factory.status.Store(sdkStatusReady)

res := client.TreatmentsWithConfigByFlagSets("user1", []string{"set1", "set2"}, nil)

expectedTreatment(res["feature"].Treatment, "TreatmentA", t)
expectedTreatment(res["feature2"].Treatment, "TreatmentB", t)
}

func TestTreatments(t *testing.T) {
factory := getFactory()
client := factory.Client()
Expand Down
Loading

0 comments on commit 6556fcc

Please sign in to comment.