Skip to content

Commit c99e8dd

Browse files
authored
feat(segment-manager): Adds implementation for ODPSegmentManager (#353)
## Summary This PR adds support for ODPSegmentManager.
1 parent f6b91bc commit c99e8dd

16 files changed

+1168
-10
lines changed

pkg/client/client.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ type OptimizelyClient struct {
5353
// CreateUserContext creates a context of the user for which decision APIs will be called.
5454
// A user context will be created successfully even when the SDK is not fully configured yet.
5555
func (o *OptimizelyClient) CreateUserContext(userID string, attributes map[string]interface{}) OptimizelyUserContext {
56+
// Passing qualified segments as nil initially since they will be fetched later
5657
return newOptimizelyUserContext(o, userID, attributes, nil, nil)
5758
}
5859

pkg/client/optimizely_user_context.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ type OptimizelyUserContext struct {
3838
}
3939

4040
// returns an instance of the optimizely user context.
41-
func newOptimizelyUserContext(optimizely *OptimizelyClient, userID string, attributes map[string]interface{}, qualifiedSegments []string, forcedDecisionService *pkgDecision.ForcedDecisionService) OptimizelyUserContext {
41+
func newOptimizelyUserContext(optimizely *OptimizelyClient, userID string, attributes map[string]interface{}, forcedDecisionService *pkgDecision.ForcedDecisionService, qualifiedSegments []string) OptimizelyUserContext {
4242
// store a copy of the provided attributes so it isn't affected by changes made afterwards.
4343
if attributes == nil {
4444
attributes = map[string]interface{}{}
@@ -115,21 +115,21 @@ func (o *OptimizelyUserContext) IsQualifiedFor(segment string) bool {
115115
// all data required to deliver the flag or experiment.
116116
func (o *OptimizelyUserContext) Decide(key string, options []decide.OptimizelyDecideOptions) OptimizelyDecision {
117117
// use a copy of the user context so that any changes to the original context are not reflected inside the decision
118-
userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes(), o.GetQualifiedSegments(), o.getForcedDecisionService())
118+
userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes(), o.getForcedDecisionService(), o.GetQualifiedSegments())
119119
return o.optimizely.decide(userContextCopy, key, convertDecideOptions(options))
120120
}
121121

122122
// DecideAll returns a key-map of decision results for all active flag keys with options.
123123
func (o *OptimizelyUserContext) DecideAll(options []decide.OptimizelyDecideOptions) map[string]OptimizelyDecision {
124124
// use a copy of the user context so that any changes to the original context are not reflected inside the decision
125-
userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes(), o.GetQualifiedSegments(), o.getForcedDecisionService())
125+
userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes(), o.getForcedDecisionService(), o.GetQualifiedSegments())
126126
return o.optimizely.decideAll(userContextCopy, convertDecideOptions(options))
127127
}
128128

129129
// DecideForKeys returns a key-map of decision results for multiple flag keys and options.
130130
func (o *OptimizelyUserContext) DecideForKeys(keys []string, options []decide.OptimizelyDecideOptions) map[string]OptimizelyDecision {
131131
// use a copy of the user context so that any changes to the original context are not reflected inside the decision
132-
userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes(), o.GetQualifiedSegments(), o.getForcedDecisionService())
132+
userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes(), o.getForcedDecisionService(), o.GetQualifiedSegments())
133133
return o.optimizely.decideForKeys(userContextCopy, keys, convertDecideOptions(options))
134134
}
135135

@@ -192,5 +192,5 @@ func copyQualifiedSegments(qualifiedSegments []string) (qualifiedSegmentsCopy []
192192
}
193193
qualifiedSegmentsCopy = make([]string, len(qualifiedSegments))
194194
copy(qualifiedSegmentsCopy, qualifiedSegments)
195-
return qualifiedSegmentsCopy
195+
return
196196
}

pkg/client/optimizely_user_context_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func (s *OptimizelyUserContextTestSuite) SetupTest() {
5858
func (s *OptimizelyUserContextTestSuite) TestOptimizelyUserContextWithAttributesAndSegments() {
5959
attributes := map[string]interface{}{"key1": 1212, "key2": 1213}
6060
segments := []string{"123"}
61-
optimizelyUserContext := newOptimizelyUserContext(s.OptimizelyClient, s.userID, attributes, segments, nil)
61+
optimizelyUserContext := newOptimizelyUserContext(s.OptimizelyClient, s.userID, attributes, nil, segments)
6262

6363
s.Equal(s.OptimizelyClient, optimizelyUserContext.GetOptimizely())
6464
s.Equal(s.userID, optimizelyUserContext.GetUserID())
@@ -80,7 +80,7 @@ func (s *OptimizelyUserContextTestSuite) TestOptimizelyUserContextNoAttributesAn
8080
func (s *OptimizelyUserContextTestSuite) TestUpatingProvidedUserContextHasNoImpactOnOptimizelyUserContext() {
8181
attributes := map[string]interface{}{"k1": "v1", "k2": false}
8282
segments := []string{"123"}
83-
optimizelyUserContext := newOptimizelyUserContext(s.OptimizelyClient, s.userID, attributes, segments, nil)
83+
optimizelyUserContext := newOptimizelyUserContext(s.OptimizelyClient, s.userID, attributes, nil, segments)
8484

8585
s.Equal(s.OptimizelyClient, optimizelyUserContext.GetOptimizely())
8686
s.Equal(s.userID, optimizelyUserContext.GetUserID())
@@ -177,7 +177,7 @@ func (s *OptimizelyUserContextTestSuite) TestSetAndGetQualifiedSegments() {
177177
userID := "1212121"
178178
var attributes map[string]interface{}
179179
qualifiedSegments := []string{"1", "2", "3"}
180-
optimizelyUserContext := newOptimizelyUserContext(s.OptimizelyClient, userID, attributes, []string{}, nil)
180+
optimizelyUserContext := newOptimizelyUserContext(s.OptimizelyClient, userID, attributes, nil, []string{})
181181
s.Len(optimizelyUserContext.GetQualifiedSegments(), 0)
182182

183183
optimizelyUserContext.SetQualifiedSegments(nil)

pkg/config/datafileprojectconfig/mappers/audience.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ func MapAudiences(audiences []datafileEntities.Audience) (audienceMap map[string
4343
if err == nil {
4444
audience.ConditionTree = conditionTree
4545
}
46+
// Only add unique segments to the list
4647
for _, s := range fSegments {
4748
if !odpSegmentsMap[s] {
4849
odpSegmentsMap[s] = true

pkg/config/datafileprojectconfig/mappers/condition_trees.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ func buildConditionTree(conditions interface{}) (conditionTree *entities.TreeNod
8383
retErr = err
8484
return
8585
}
86+
// Extract odp segment from leaf node if applicable
8687
extractSegment(&odpSegments, n)
8788
}
8889

@@ -102,6 +103,7 @@ func buildConditionTree(conditions interface{}) (conditionTree *entities.TreeNod
102103
retErr = err
103104
return
104105
}
106+
// Extract odp segment from leaf node if applicable
105107
extractSegment(&odpSegments, n)
106108
conditionTree.Operator = "or"
107109
conditionTree.Nodes = append(conditionTree.Nodes, n)
@@ -209,11 +211,13 @@ func isValidOperator(op string) bool {
209211
return false
210212
}
211213

214+
// Extracts odpSegment from leaf node and adds it to odpSegments array
212215
func extractSegment(odpSegments *[]string, node *entities.TreeNode) {
213216
condition, ok := node.Item.(entities.Condition)
214217
if !ok {
215218
return
216219
}
220+
// Add segment to list only if match type is qualified and value is a non-empty string
217221
if condition.Match == matchers.QualifiedMatchType {
218222
if segment, ok := condition.Value.(string); ok && segment != "" {
219223
*odpSegments = append(*odpSegments, segment)

pkg/odp/config.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/****************************************************************************
2+
* Copyright 2022, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
17+
// Package odp //
18+
package odp
19+
20+
import (
21+
"sync"
22+
23+
"github.com/optimizely/go-sdk/pkg/odp/utils"
24+
)
25+
26+
// Config is used to represent odp config
27+
type Config interface {
28+
Update(apiKey, apiHost string, segmentsToCheck []string) bool
29+
GetAPIKey() string
30+
GetAPIHost() string
31+
GetSegmentsToCheck() []string
32+
IsOdpServiceIntegrated() bool
33+
}
34+
35+
// DefaultConfig represents default implementation of odp config
36+
type DefaultConfig struct {
37+
apiKey, apiHost string
38+
segmentsToCheck []string
39+
lock sync.RWMutex
40+
}
41+
42+
// NewConfig creates and returns a new instance of DefaultConfig.
43+
func NewConfig(apiKey, apiHost string, segmentsToCheck []string) *DefaultConfig {
44+
return &DefaultConfig{
45+
apiKey: apiKey,
46+
apiHost: apiHost,
47+
segmentsToCheck: segmentsToCheck,
48+
}
49+
}
50+
51+
// Update updates config.
52+
func (s *DefaultConfig) Update(apiKey, apiHost string, segmentsToCheck []string) bool {
53+
s.lock.Lock()
54+
defer s.lock.Unlock()
55+
56+
if s.apiKey == apiKey && s.apiHost == apiHost && utils.Equal(s.segmentsToCheck, segmentsToCheck) {
57+
return false
58+
}
59+
s.apiKey = apiKey
60+
s.apiHost = apiHost
61+
s.segmentsToCheck = segmentsToCheck
62+
return true
63+
}
64+
65+
// GetAPIKey returns value for APIKey.
66+
func (s *DefaultConfig) GetAPIKey() string {
67+
s.lock.RLock()
68+
defer s.lock.RUnlock()
69+
return s.apiKey
70+
}
71+
72+
// GetAPIHost returns value for APIHost.
73+
func (s *DefaultConfig) GetAPIHost() string {
74+
s.lock.RLock()
75+
defer s.lock.RUnlock()
76+
return s.apiHost
77+
}
78+
79+
// GetSegmentsToCheck returns an array of all ODP segments used in the current datafile (associated with apiHost/apiKey).
80+
func (s *DefaultConfig) GetSegmentsToCheck() []string {
81+
s.lock.RLock()
82+
defer s.lock.RUnlock()
83+
if s.segmentsToCheck == nil {
84+
return nil
85+
}
86+
segmentsToCheck := make([]string, len(s.segmentsToCheck))
87+
copy(segmentsToCheck, s.segmentsToCheck)
88+
return segmentsToCheck
89+
}
90+
91+
// IsOdpServiceIntegrated returns true if odp service is integrated
92+
func (s *DefaultConfig) IsOdpServiceIntegrated() bool {
93+
return s.GetAPIHost() != "" && s.GetAPIKey() != ""
94+
}

pkg/odp/config_test.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/****************************************************************************
2+
* Copyright 2022, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
17+
// Package odp //
18+
package odp
19+
20+
import (
21+
"fmt"
22+
"sync"
23+
"testing"
24+
25+
"github.com/stretchr/testify/suite"
26+
)
27+
28+
type ConfigTestSuite struct {
29+
suite.Suite
30+
apiHost, apiKey string
31+
segmentsToCheck []string
32+
config *DefaultConfig
33+
}
34+
35+
func (c *ConfigTestSuite) SetupTest() {
36+
c.apiHost = "test-host"
37+
c.apiKey = "test-api-key"
38+
c.segmentsToCheck = []string{"a", "b", "c"}
39+
c.config = NewConfig(c.apiKey, c.apiHost, c.segmentsToCheck)
40+
}
41+
42+
func (c *ConfigTestSuite) TestNewConfigWithValidValues() {
43+
c.Equal(c.apiHost, c.config.GetAPIHost())
44+
c.Equal(c.apiKey, c.config.GetAPIKey())
45+
c.Equal(c.segmentsToCheck, c.config.GetSegmentsToCheck())
46+
c.True(c.config.IsOdpServiceIntegrated())
47+
}
48+
49+
func (c *ConfigTestSuite) TestNewConfigWithEmptyValues() {
50+
c.config = NewConfig("", "", nil)
51+
c.Equal("", c.config.GetAPIHost())
52+
c.Equal("", c.config.GetAPIKey())
53+
c.Nil(c.config.GetSegmentsToCheck())
54+
c.False(c.config.IsOdpServiceIntegrated())
55+
}
56+
57+
func (c *ConfigTestSuite) TestUpdateWithValidValues() {
58+
expectedAPIKey := "1"
59+
expectedAPIHost := "2"
60+
expectedSegmentsList := []string{"1", "2", "3"}
61+
c.True(c.config.Update(expectedAPIKey, expectedAPIHost, expectedSegmentsList))
62+
c.Equal(expectedAPIKey, c.config.GetAPIKey())
63+
c.Equal(expectedAPIHost, c.config.GetAPIHost())
64+
c.Equal(expectedSegmentsList, c.config.GetSegmentsToCheck())
65+
c.True(c.config.IsOdpServiceIntegrated())
66+
}
67+
68+
func (c *ConfigTestSuite) TestUpdateWithEmptyValues() {
69+
expectedAPIKey := "1"
70+
expectedAPIHost := ""
71+
var expectedSegmentsList []string
72+
c.True(c.config.Update(expectedAPIKey, expectedAPIHost, expectedSegmentsList))
73+
c.Equal(expectedAPIKey, c.config.GetAPIKey())
74+
c.Equal(expectedAPIHost, c.config.GetAPIHost())
75+
c.Equal(expectedSegmentsList, c.config.GetSegmentsToCheck())
76+
c.False(c.config.IsOdpServiceIntegrated())
77+
}
78+
79+
func (c *ConfigTestSuite) TestUpdateWithSameValues() {
80+
c.False(c.config.Update(c.apiKey, c.apiHost, c.segmentsToCheck))
81+
c.Equal(c.apiKey, c.config.GetAPIKey())
82+
c.Equal(c.apiHost, c.config.GetAPIHost())
83+
c.Equal(c.segmentsToCheck, c.config.GetSegmentsToCheck())
84+
c.True(c.config.IsOdpServiceIntegrated())
85+
}
86+
87+
func (c *ConfigTestSuite) TestRaceCondition() {
88+
wg := sync.WaitGroup{}
89+
update := func(i int) {
90+
v := fmt.Sprintf("%d", i)
91+
c.config.Update(v, v, []string{v})
92+
wg.Done()
93+
}
94+
95+
getAPIKey := func() {
96+
c.config.GetAPIKey()
97+
wg.Done()
98+
}
99+
100+
getAPIHost := func() {
101+
c.config.GetAPIHost()
102+
wg.Done()
103+
}
104+
105+
getSegmentsToCheck := func() {
106+
c.config.GetSegmentsToCheck()
107+
wg.Done()
108+
}
109+
110+
isOdpServiceIntegrated := func() {
111+
c.config.IsOdpServiceIntegrated()
112+
wg.Done()
113+
}
114+
115+
iterations := 5
116+
wg.Add(iterations * 5)
117+
118+
for i := 0; i < iterations; i++ {
119+
go update(i)
120+
go getAPIKey()
121+
go getAPIHost()
122+
go getSegmentsToCheck()
123+
go isOdpServiceIntegrated()
124+
}
125+
wg.Wait()
126+
}
127+
128+
func TestConfigTestSuite(t *testing.T) {
129+
suite.Run(t, new(ConfigTestSuite))
130+
}

pkg/odp/errors.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/****************************************************************************
2+
* Copyright 2022, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
17+
// Package odp //
18+
package odp
19+
20+
const invalidSegmentIdentifier = "audience segments fetch failed (invalid identifier)"
21+
const fetchSegmentsFailedError = "audience segments fetch failed (%s)"

pkg/odp/lru_cache.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ type LRUCache struct {
4646
}
4747

4848
// NewLRUCache returns a new instance of Least Recently Used in-memory cache
49-
func NewLRUCache(size int, timeoutInSecs int64) LRUCache {
50-
return LRUCache{queue: list.New(), items: make(map[string]*cacheElement), maxSize: size, timeoutInSecs: timeoutInSecs, lock: new(sync.RWMutex)}
49+
func NewLRUCache(size int, timeoutInSecs int64) *LRUCache {
50+
return &LRUCache{queue: list.New(), items: make(map[string]*cacheElement), maxSize: size, timeoutInSecs: timeoutInSecs, lock: new(sync.RWMutex)}
5151
}
5252

5353
// Save stores a new element into the cache

pkg/odp/lru_cache_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,3 +187,14 @@ func TestTimeout(t *testing.T) {
187187
assert.Equal(t, 200, cache2.Lookup("2"))
188188
assert.Equal(t, 300, cache2.Lookup("3"))
189189
}
190+
191+
type TestCache struct {
192+
}
193+
194+
func (l *TestCache) Save(key string, value interface{}) {
195+
}
196+
func (l *TestCache) Lookup(key string) interface{} {
197+
return nil
198+
}
199+
func (l *TestCache) Reset() {
200+
}

0 commit comments

Comments
 (0)