diff --git a/pkg/config/datafileprojectconfig/mappers/experiment.go b/pkg/config/datafileprojectconfig/mappers/experiment.go index e8efd835..b17f687d 100644 --- a/pkg/config/datafileprojectconfig/mappers/experiment.go +++ b/pkg/config/datafileprojectconfig/mappers/experiment.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019,2021, Optimizely, Inc. and contributors * + * Copyright 2019,2021-2022, 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. * @@ -83,6 +83,7 @@ func mapExperiment(rawExperiment datafileEntities.Experiment) entities.Experimen AudienceIds: rawExperiment.AudienceIds, AudienceConditions: rawExperiment.AudienceConditions, ID: rawExperiment.ID, + IsExperimentRunning: rawExperiment.Status == "Running", LayerID: rawExperiment.LayerID, Key: rawExperiment.Key, Variations: make(map[string]entities.Variation), diff --git a/pkg/config/datafileprojectconfig/mappers/experiment_test.go b/pkg/config/datafileprojectconfig/mappers/experiment_test.go index 421c6a96..f062a78f 100644 --- a/pkg/config/datafileprojectconfig/mappers/experiment_test.go +++ b/pkg/config/datafileprojectconfig/mappers/experiment_test.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019,2021, Optimizely, Inc. and contributors * + * Copyright 2019,2021-2022, 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. * @@ -29,6 +29,7 @@ func TestMapExperiments(t *testing.T) { const testExperimentString = `{ "audienceIds": ["31111"], "id": "11111", + "status": "Running", "key": "test_experiment_11111", "variations": [ { @@ -70,10 +71,11 @@ func TestMapExperiments(t *testing.T) { experimentsIDMap, experimentKeyMap := MapExperiments(rawExperiments, experimentGroupMap) expectedExperiments := map[string]entities.Experiment{ "11111": { - AudienceIds: []string{"31111"}, - ID: "11111", - GroupID: "15", - Key: "test_experiment_11111", + AudienceIds: []string{"31111"}, + ID: "11111", + IsExperimentRunning: true, + GroupID: "15", + Key: "test_experiment_11111", Variations: map[string]entities.Variation{ "21111": { ID: "21111", @@ -129,6 +131,7 @@ func TestMapExperimentsWithStringAudienceCondition(t *testing.T) { AudienceIds: []string{"31111"}, Key: "test_experiment_11111", AudienceConditions: "31111", + Status: "Paused", } rawExperiments := []datafileEntities.Experiment{rawExperiment} @@ -139,6 +142,7 @@ func TestMapExperimentsWithStringAudienceCondition(t *testing.T) { "11111": { AudienceIds: []string{"31111"}, ID: "11111", + IsExperimentRunning: false, GroupID: "15", Key: "test_experiment_11111", Variations: map[string]entities.Variation{}, diff --git a/pkg/config/datafileprojectconfig/mappers/rollout_test.go b/pkg/config/datafileprojectconfig/mappers/rollout_test.go index 373dcb27..47bef1f2 100644 --- a/pkg/config/datafileprojectconfig/mappers/rollout_test.go +++ b/pkg/config/datafileprojectconfig/mappers/rollout_test.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019,2021 Optimizely, Inc. and contributors * + * Copyright 2019,2021-2022 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. * @@ -28,8 +28,9 @@ func TestMapRollouts(t *testing.T) { const testRolloutString = `{ "id": "21111", "experiments": [ - { "id": "11111", "key": "exp_11111" }, - { "id": "11112", "key": "exp_11112" } + { "id": "11111", "key": "exp_11111", "status": "Running"}, + { "id": "11112", "key": "exp_11112", "status": "Paused"}, + { "id": "11113", "key": "exp_11113", "status": ""} ] }` @@ -42,8 +43,9 @@ func TestMapRollouts(t *testing.T) { "21111": { ID: "21111", Experiments: []entities.Experiment{ - {ID: "11111", Key: "exp_11111", Variations: map[string]entities.Variation{}, VariationKeyToIDMap: map[string]string{}, TrafficAllocation: []entities.Range{}}, - {ID: "11112", Key: "exp_11112", Variations: map[string]entities.Variation{}, VariationKeyToIDMap: map[string]string{}, TrafficAllocation: []entities.Range{}}, + {ID: "11111", Key: "exp_11111", Variations: map[string]entities.Variation{}, VariationKeyToIDMap: map[string]string{}, TrafficAllocation: []entities.Range{}, IsExperimentRunning: true}, + {ID: "11112", Key: "exp_11112", Variations: map[string]entities.Variation{}, VariationKeyToIDMap: map[string]string{}, TrafficAllocation: []entities.Range{}, IsExperimentRunning: false}, + {ID: "11113", Key: "exp_11113", Variations: map[string]entities.Variation{}, VariationKeyToIDMap: map[string]string{}, TrafficAllocation: []entities.Range{}, IsExperimentRunning: false}, }, }, } diff --git a/pkg/decision/helpers_test.go b/pkg/decision/helpers_test.go index 70cc4441..8158f4b8 100644 --- a/pkg/decision/helpers_test.go +++ b/pkg/decision/helpers_test.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019-2021, Optimizely, Inc. and contributors * + * Copyright 2019-2022, 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. * @@ -120,6 +120,7 @@ var testExp1111 = entities.Experiment{ TrafficAllocation: []entities.Range{ {EntityID: "2222", EndOfRange: 10000}, }, + IsExperimentRunning: true, } // Simple feature test @@ -159,6 +160,7 @@ var testExp1112 = entities.Experiment{ TrafficAllocation: []entities.Range{ {EntityID: "2222", EndOfRange: 10000}, }, + IsExperimentRunning: true, } var testExp1117Var2223 = entities.Variation{ID: "2223", Key: "2223"} var testAudience5556 = entities.Audience{ID: "5556"} @@ -180,6 +182,7 @@ var testExp1117 = entities.Experiment{ TrafficAllocation: []entities.Range{ {EntityID: "2223", EndOfRange: 10000}, }, + IsExperimentRunning: true, } var testExp1118Var2224 = entities.Variation{ID: "2224", Key: "2224"} var testAudience5557 = entities.Audience{ID: "5557"} @@ -201,6 +204,7 @@ var testExp1118 = entities.Experiment{ TrafficAllocation: []entities.Range{ {EntityID: "2224", EndOfRange: 10000}, }, + IsExperimentRunning: true, } const testFeatRollout3334Key = "test_feature_rollout_3334_key" diff --git a/pkg/decision/rollout_service.go b/pkg/decision/rollout_service.go index 4be4352d..8e56539d 100644 --- a/pkg/decision/rollout_service.go +++ b/pkg/decision/rollout_service.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019-2021, Optimizely, Inc. and contributors * + * Copyright 2019-2022, 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. * @@ -102,6 +102,11 @@ func (r RolloutService) GetDecision(decisionContext FeatureDecisionContext, user loggingKey := strconv.Itoa(index + 1) experiment := &rollout.Experiments[index] + // Only evaluate if experiment is running + if !experiment.IsExperimentRunning { + continue + } + // Checking for forced decision if forcedDecision := checkForForcedDecision(experiment); forcedDecision != nil { return *forcedDecision, reasons, nil diff --git a/pkg/decision/rollout_service_test.go b/pkg/decision/rollout_service_test.go index 9bf160db..a50eb2e6 100644 --- a/pkg/decision/rollout_service_test.go +++ b/pkg/decision/rollout_service_test.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019-2021, Optimizely, Inc. and contributors * + * Copyright 2019-2022, 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. * @@ -117,6 +117,52 @@ func (s *RolloutServiceTestSuite) TestGetDecisionWithNoExperiments() { s.Equal(expectedFeatureDecision, decision) } +func (s *RolloutServiceTestSuite) TestGetDecisionWithPausedExperiment() { + // Test experiment passes targeting and bucketing + testExperimentBucketerDecision := ExperimentDecision{ + Variation: &testExp1117Var2223, + Decision: Decision{Reason: reasons.BucketedIntoVariation}, + } + + // Pausing first rollout experiment + featureRollout := &testFeatRollout3334 + featureRollout.Rollout.Experiments[0].IsExperimentRunning = false + + s.testFeatureDecisionContext = FeatureDecisionContext{ + Feature: featureRollout, + ProjectConfig: s.mockConfig, + ForcedDecisionService: NewForcedDecisionService("test_user"), + } + + testExperiment1117DecisionContext := ExperimentDecisionContext{ + Experiment: &testExp1117, + ProjectConfig: s.mockConfig, + } + + s.mockAudienceTreeEvaluator.On("Evaluate", testExp1117.AudienceConditionTree, s.testConditionTreeParams, mock.Anything).Return(true, true, s.reasons) + s.mockExperimentService.On("GetDecision", testExperiment1117DecisionContext, s.testUserContext, s.options, mock.Anything).Return(testExperimentBucketerDecision, s.reasons, nil) + + testRolloutService := RolloutService{ + audienceTreeEvaluator: s.mockAudienceTreeEvaluator, + experimentBucketerService: s.mockExperimentService, + logger: s.mockLogger, + } + expectedFeatureDecision := FeatureDecision{ + Experiment: testExp1117, + Variation: &testExp1117Var2223, + Source: Rollout, + Decision: Decision{Reason: reasons.BucketedIntoRollout}, + } + s.mockLogger.On("Debug", fmt.Sprintf(logging.EvaluatingAudiencesForRollout.String(), "2")) + s.mockLogger.On("Debug", fmt.Sprintf(logging.RolloutAudiencesEvaluatedTo.String(), "2", true)) + s.mockLogger.On("Debug", `Decision made for user "test_user" for feature rollout with key "test_feature_rollout_3334_key": Bucketed into feature rollout.`) + decision, _, _ := testRolloutService.GetDecision(s.testFeatureDecisionContext, s.testUserContext, s.options) + s.Equal(expectedFeatureDecision, decision) + s.mockAudienceTreeEvaluator.AssertExpectations(s.T()) + s.mockExperimentService.AssertExpectations(s.T()) + s.mockLogger.AssertExpectations(s.T()) +} + func (s *RolloutServiceTestSuite) TestGetDecisionHappyPath() { // Test experiment passes targeting and bucketing testExperimentBucketerDecision := ExperimentDecision{ diff --git a/pkg/entities/experiment.go b/pkg/entities/experiment.go index 24692ae1..80fae997 100644 --- a/pkg/entities/experiment.go +++ b/pkg/entities/experiment.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019,2021, Optimizely, Inc. and contributors * + * Copyright 2019,2022, 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. * @@ -30,6 +30,8 @@ type Experiment struct { AudienceIds []string AudienceConditions interface{} ID string + IsExperimentRunning bool + IsFeatureExperiment bool LayerID string Key string Variations map[string]Variation // keyed by variation ID @@ -38,7 +40,6 @@ type Experiment struct { GroupID string AudienceConditionTree *TreeNode Whitelist map[string]string - IsFeatureExperiment bool } // Range represents bucketing range that the specify entityID falls into