Skip to content

Commit fadd0ec

Browse files
authored
fix: Update built-in map-based ExperimentOverrideStore implementation to be concurrently usable (#171)
The built-in default implementation of ExperimentOverrideStore should be production ready, so it should be safe to use concurrently. In this PR: - The default implementation is renamed to MapExperimentOverridesStore. - A sync.RWLock is added to MapExperimentOverridesStore. - A SetVariation method is added for setting an override, which uses the mutex. - The GetVariation method is updated to use the mutex. - A new test is added that calls GetVariation and SetVariation concurrently from several goroutines.
1 parent dc6c346 commit fadd0ec

File tree

4 files changed

+84
-16
lines changed

4 files changed

+84
-16
lines changed

pkg/client/factory_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ func TestClientWithCustomDecisionServiceOptions(t *testing.T) {
151151
factory := OptimizelyFactory{SDKKey: "1212"}
152152

153153
mockUserProfileService := new(MockUserProfileService)
154-
mockOverrideStore := new(decision.MapOverridesStore)
154+
mockOverrideStore := new(decision.MapExperimentOverridesStore)
155155
optimizelyClient, err := factory.Client(
156156
WithUserProfileService(mockUserProfileService),
157157
WithExperimentOverrides(mockOverrideStore),

pkg/decision/composite_experiment_service_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ func (s *CompositeExperimentTestSuite) TestNewCompositeExperimentService() {
160160

161161
func (s *CompositeExperimentTestSuite) TestNewCompositeExperimentServiceWithCustomOptions() {
162162
mockUserProfileService := new(MockUserProfileService)
163-
mockExperimentOverrideStore := new(MapOverridesStore)
163+
mockExperimentOverrideStore := new(MapExperimentOverridesStore)
164164
compositeExperimentService := NewCompositeExperimentService(
165165
WithUserProfileService(mockUserProfileService),
166166
WithOverrideStore(mockExperimentOverrideStore),

pkg/decision/experiment_override_service.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ package decision
2020
import (
2121
"errors"
2222
"fmt"
23+
"sync"
2324

2425
"github.com/optimizely/go-sdk/pkg/decision/reasons"
2526
"github.com/optimizely/go-sdk/pkg/entities"
@@ -39,17 +40,27 @@ type ExperimentOverrideStore interface {
3940
GetVariation(overrideKey ExperimentOverrideKey) (string, bool)
4041
}
4142

42-
// MapOverridesStore is a map-based implementation of OverrideStore
43-
type MapOverridesStore struct {
43+
// MapExperimentOverridesStore is a map-based implementation of ExperimentOverridesStore that is safe to use concurrently
44+
type MapExperimentOverridesStore struct {
4445
overridesMap map[ExperimentOverrideKey]string
46+
mutex sync.RWMutex
4547
}
4648

47-
// GetVariation returns the override associated with the given key in the map
48-
func (m *MapOverridesStore) GetVariation(overrideKey ExperimentOverrideKey) (string, bool) {
49+
// GetVariation returns the override variation key associated with the given user+experiment key
50+
func (m *MapExperimentOverridesStore) GetVariation(overrideKey ExperimentOverrideKey) (string, bool) {
51+
m.mutex.RLock()
4952
variationKey, ok := m.overridesMap[overrideKey]
53+
m.mutex.RUnlock()
5054
return variationKey, ok
5155
}
5256

57+
// SetVariation sets the given variation key as an override for the given user+experiment key
58+
func (m *MapExperimentOverridesStore) SetVariation(overrideKey ExperimentOverrideKey, variationKey string) {
59+
m.mutex.Lock()
60+
m.overridesMap[overrideKey] = variationKey
61+
m.mutex.Unlock()
62+
}
63+
5364
// ExperimentOverrideService makes a decision using an ExperimentOverridesStore
5465
// Implements the ExperimentService interface
5566
type ExperimentOverrideService struct {

pkg/decision/experiment_override_service_test.go

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package decision
1919

2020
import (
21+
"sync"
2122
"testing"
2223

2324
"github.com/optimizely/go-sdk/pkg/decision/reasons"
@@ -28,16 +29,16 @@ import (
2829
type ExperimentOverrideServiceTestSuite struct {
2930
suite.Suite
3031
mockConfig *mockProjectConfig
31-
overrides map[ExperimentOverrideKey]string
32+
overrides *MapExperimentOverridesStore
3233
overrideService *ExperimentOverrideService
3334
}
3435

3536
func (s *ExperimentOverrideServiceTestSuite) SetupTest() {
3637
s.mockConfig = new(mockProjectConfig)
37-
s.overrides = make(map[ExperimentOverrideKey]string)
38-
s.overrideService = NewExperimentOverrideService(&MapOverridesStore{
39-
overridesMap: s.overrides,
40-
})
38+
s.overrides = &MapExperimentOverridesStore{
39+
overridesMap: make(map[ExperimentOverrideKey]string),
40+
}
41+
s.overrideService = NewExperimentOverrideService(s.overrides)
4142
}
4243

4344
func (s *ExperimentOverrideServiceTestSuite) TestOverridesIncludeVariation() {
@@ -48,7 +49,7 @@ func (s *ExperimentOverrideServiceTestSuite) TestOverridesIncludeVariation() {
4849
testUserContext := entities.UserContext{
4950
ID: "test_user_1",
5051
}
51-
s.overrides[ExperimentOverrideKey{ExperimentKey: testExp1111.Key, UserID: "test_user_1"}] = testExp1111Var2222.Key
52+
s.overrides.SetVariation(ExperimentOverrideKey{ExperimentKey: testExp1111.Key, UserID: "test_user_1"}, testExp1111Var2222.Key)
5253
decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext)
5354
s.NoError(err)
5455
s.NotNil(decision.Variation)
@@ -77,7 +78,7 @@ func (s *ExperimentOverrideServiceTestSuite) TestNoOverrideForExperiment() {
7778
ID: "test_user_1",
7879
}
7980
// The decision context refers to testExp1111, but this override is for another experiment
80-
s.overrides[ExperimentOverrideKey{ExperimentKey: testExp1113.Key, UserID: "test_user_1"}] = testExp1113Var2224.Key
81+
s.overrides.SetVariation(ExperimentOverrideKey{ExperimentKey: testExp1113.Key, UserID: "test_user_1"}, testExp1113Var2224.Key)
8182
decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext)
8283
s.NoError(err)
8384
s.Nil(decision.Variation)
@@ -93,7 +94,7 @@ func (s *ExperimentOverrideServiceTestSuite) TestNoOverrideForUser() {
9394
ID: "test_user_1",
9495
}
9596
// The user context refers to "test_user_1", but this override is for another user
96-
s.overrides[ExperimentOverrideKey{ExperimentKey: testExp1111.Key, UserID: "test_user_2"}] = testExp1111Var2222.Key
97+
s.overrides.SetVariation(ExperimentOverrideKey{ExperimentKey: testExp1111.Key, UserID: "test_user_2"}, testExp1111Var2222.Key)
9798
decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext)
9899
s.NoError(err)
99100
s.Nil(decision.Variation)
@@ -109,7 +110,7 @@ func (s *ExperimentOverrideServiceTestSuite) TestNoOverrideForUserOrExperiment()
109110
ID: "test_user_1",
110111
}
111112
// This override is for both a different user and a different experiment than the ones in the contexts above
112-
s.overrides[ExperimentOverrideKey{ExperimentKey: testExp1113.Key, UserID: "test_user_3"}] = testExp1111Var2222.Key
113+
s.overrides.SetVariation(ExperimentOverrideKey{ExperimentKey: testExp1113.Key, UserID: "test_user_3"}, testExp1111Var2222.Key)
113114
decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext)
114115
s.NoError(err)
115116
s.Nil(decision.Variation)
@@ -125,13 +126,69 @@ func (s *ExperimentOverrideServiceTestSuite) TestInvalidVariationInOverride() {
125126
ID: "test_user_1",
126127
}
127128
// This override variation key does not exist in the experiment
128-
s.overrides[ExperimentOverrideKey{ExperimentKey: testExp1111.Key, UserID: "test_user_1"}] = "invalid_variation_key"
129+
s.overrides.SetVariation(ExperimentOverrideKey{ExperimentKey: testExp1111.Key, UserID: "test_user_1"}, "invalid_variation_key")
129130
decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext)
130131
s.NoError(err)
131132
s.Nil(decision.Variation)
132133
s.Exactly(reasons.InvalidOverrideVariationAssignment, decision.Reason)
133134
}
134135

136+
// Test concurrent use of the MapExperimentOverrideStore
137+
// Create 3 goroutines that set and get variations, and assert that all their sets take effect at the end
138+
func (s *ExperimentOverrideServiceTestSuite) TestMapExperimentOverridesStoreConcurrent() {
139+
testDecisionContext := ExperimentDecisionContext{
140+
Experiment: &testExp1111,
141+
ProjectConfig: s.mockConfig,
142+
}
143+
var wg sync.WaitGroup
144+
wg.Add(1)
145+
go func() {
146+
s.overrides.SetVariation(ExperimentOverrideKey{ExperimentKey: testExp1111.Key, UserID: "test_user_1"}, testExp1111Var2222.Key)
147+
user1Decision, _ := s.overrideService.GetDecision(testDecisionContext, entities.UserContext{
148+
ID: "test_user_1",
149+
})
150+
s.NotNil(user1Decision.Variation)
151+
s.Exactly(testExp1111Var2222.Key, user1Decision.Variation.Key)
152+
wg.Done()
153+
}()
154+
wg.Add(1)
155+
go func() {
156+
s.overrides.SetVariation(ExperimentOverrideKey{ExperimentKey: testExp1111.Key, UserID: "test_user_2"}, testExp1111Var2222.Key)
157+
user2Decision, _ := s.overrideService.GetDecision(testDecisionContext, entities.UserContext{
158+
ID: "test_user_2",
159+
})
160+
s.NotNil(user2Decision.Variation)
161+
s.Exactly(testExp1111Var2222.Key, user2Decision.Variation.Key)
162+
wg.Done()
163+
}()
164+
wg.Add(1)
165+
go func() {
166+
s.overrides.SetVariation(ExperimentOverrideKey{ExperimentKey: testExp1111.Key, UserID: "test_user_3"}, testExp1111Var2222.Key)
167+
user3Decision, _ := s.overrideService.GetDecision(testDecisionContext, entities.UserContext{
168+
ID: "test_user_3",
169+
})
170+
s.NotNil(user3Decision.Variation)
171+
s.Exactly(testExp1111Var2222.Key, user3Decision.Variation.Key)
172+
wg.Done()
173+
}()
174+
wg.Wait()
175+
user1Decision, _ := s.overrideService.GetDecision(testDecisionContext, entities.UserContext{
176+
ID: "test_user_1",
177+
})
178+
user2Decision, _ := s.overrideService.GetDecision(testDecisionContext, entities.UserContext{
179+
ID: "test_user_2",
180+
})
181+
user3Decision, _ := s.overrideService.GetDecision(testDecisionContext, entities.UserContext{
182+
ID: "test_user_3",
183+
})
184+
s.NotNil(user1Decision.Variation)
185+
s.Exactly(testExp1111Var2222.Key, user1Decision.Variation.Key)
186+
s.NotNil(user2Decision.Variation)
187+
s.Exactly(testExp1111Var2222.Key, user2Decision.Variation.Key)
188+
s.NotNil(user3Decision.Variation)
189+
s.Exactly(testExp1111Var2222.Key, user3Decision.Variation.Key)
190+
}
191+
135192
func TestExperimentOverridesTestSuite(t *testing.T) {
136193
suite.Run(t, new(ExperimentOverrideServiceTestSuite))
137194
}

0 commit comments

Comments
 (0)