Skip to content

Commit 308b1e4

Browse files
pawels-optimizelyMichael Ng
authored andcommitted
added initial code for polling manager (#48)
1 parent c715e78 commit 308b1e4

File tree

9 files changed

+652
-6
lines changed

9 files changed

+652
-6
lines changed

cmd/is_feature_enabled.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ var isFeatureEnabledCmd = &cobra.Command{
3838
SDKKey: sdkKey,
3939
}
4040

41-
client, err := optimizelyFactory.Client()
41+
client, err := optimizelyFactory.StaticClient()
4242

4343
if err != nil {
4444
fmt.Printf("Error instantiating client: %s\n", err)

examples/main.go

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"context"
45
"fmt"
56
"time"
67

@@ -15,7 +16,10 @@ func main() {
1516
optimizelyFactory := &client.OptimizelyFactory{
1617
SDKKey: "4SLpaJA1r1pgE6T2CoMs9q",
1718
}
18-
client, err := optimizelyFactory.Client()
19+
20+
/************* StaticClient ********************/
21+
22+
app, err := optimizelyFactory.StaticClient()
1923

2024
if err != nil {
2125
fmt.Printf("Error instantiating client: %s", err)
@@ -30,8 +34,8 @@ func main() {
3034
},
3135
}
3236

33-
enabled, _ := client.IsFeatureEnabled("mutext_feat", user)
34-
fmt.Printf("Is feature enabled? %v", enabled)
37+
enabled, _ := app.IsFeatureEnabled("mutext_feat", user)
38+
fmt.Printf("Is feature enabled? %v\n", enabled)
3539

3640
processor := event.NewEventProcessor(100, 100)
3741

@@ -45,4 +49,23 @@ func main() {
4549
time.Sleep(1000 * time.Millisecond)
4650
fmt.Println("\nending")
4751
}
52+
53+
/************* ClientWithContext ********************/
54+
55+
optimizelyFactory = &client.OptimizelyFactory{
56+
SDKKey: "4SLpaJA1r1pgE6T2CoMs9q",
57+
}
58+
ctx := context.Background()
59+
ctx, cancelManager := context.WithCancel(ctx) // user can set up any context
60+
app, err = optimizelyFactory.ClientWithContext(ctx)
61+
cancelManager() // user can cancel anytime
62+
63+
if err != nil {
64+
fmt.Printf("Error instantiating client: %s", err)
65+
return
66+
}
67+
68+
enabled, _ = app.IsFeatureEnabled("mutext_feat", user)
69+
fmt.Printf("Is feature enabled? %v\n", enabled)
70+
4871
}

optimizely/client/client.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package client
1919
import (
2020
"errors"
2121
"fmt"
22+
"reflect"
2223
"runtime/debug"
2324

2425
"github.com/optimizely/go-sdk/optimizely"
@@ -54,6 +55,10 @@ func (o *OptimizelyClient) IsFeatureEnabled(featureKey string, userContext entit
5455
}()
5556

5657
projectConfig := o.configManager.GetConfig()
58+
59+
if reflect.ValueOf(projectConfig).IsNil() {
60+
return false, fmt.Errorf("project config is null")
61+
}
5762
feature, err := projectConfig.GetFeatureByKey(featureKey)
5863
if err != nil {
5964
logger.Error("Error retrieving feature", err)

optimizely/client/factory.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package client
1818

1919
import (
20+
"context"
2021
"fmt"
2122

2223
"github.com/optimizely/go-sdk/optimizely"
@@ -30,8 +31,8 @@ type OptimizelyFactory struct {
3031
Datafile []byte
3132
}
3233

33-
// Client returns a client initialized with the defaults
34-
func (f OptimizelyFactory) Client() (*OptimizelyClient, error) {
34+
// StaticClient returns a client initialized with the defaults
35+
func (f OptimizelyFactory) StaticClient() (*OptimizelyClient, error) {
3536
var configManager optimizely.ProjectConfigManager
3637

3738
if f.SDKKey != "" {
@@ -62,3 +63,25 @@ func (f OptimizelyFactory) Client() (*OptimizelyClient, error) {
6263
}
6364
return &client, nil
6465
}
66+
67+
// ClientWithContext returns a client initialized with the defaults
68+
func (f OptimizelyFactory) ClientWithContext(ctx context.Context) (*OptimizelyClient, error) {
69+
var configManager optimizely.ProjectConfigManager
70+
71+
if f.SDKKey != "" {
72+
url := fmt.Sprintf("https://cdn.optimizely.com/datafiles/%s.json", f.SDKKey)
73+
request := config.NewRequester(url)
74+
75+
configManager = config.NewPollingProjectConfigManager(ctx, request, f.Datafile, 0)
76+
77+
decisionService := decision.NewCompositeService()
78+
client := OptimizelyClient{
79+
decisionService: decisionService,
80+
configManager: configManager,
81+
isValid: true,
82+
}
83+
return &client, nil
84+
}
85+
86+
return nil, fmt.Errorf("Cannot create ClientWithContext")
87+
}

optimizely/config/polling_manager.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/****************************************************************************
2+
* Copyright 2019, 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 config
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"sync"
23+
"time"
24+
25+
"github.com/optimizely/go-sdk/optimizely"
26+
"github.com/optimizely/go-sdk/optimizely/config/datafileProjectConfig"
27+
"github.com/optimizely/go-sdk/optimizely/logging"
28+
"github.com/optimizely/go-sdk/optimizely/utils"
29+
)
30+
31+
const defaultPollingWait = time.Duration(5 * time.Minute) // default 5 minutes for polling wait
32+
33+
var cmLogger = logging.GetLogger("PollingConfigManager")
34+
35+
// PollingProjectConfigManager maintains a dynamic copy of the project config
36+
type PollingProjectConfigManager struct {
37+
requester *Requester
38+
metrics *utils.Metrics
39+
pollingWait time.Duration
40+
projectConfig optimizely.ProjectConfig
41+
configLock sync.RWMutex
42+
43+
ctx context.Context // context used for cancellation
44+
}
45+
46+
func (cm *PollingProjectConfigManager) activate(initialPayload []byte, init bool) {
47+
48+
update := func() {
49+
var e error
50+
var code int
51+
var payload []byte
52+
if init && len(initialPayload) > 0 {
53+
payload = initialPayload
54+
} else {
55+
payload, code, e = cm.requester.Get()
56+
57+
if e != nil {
58+
cm.metrics.Inc("bad_http_request")
59+
cmLogger.Error(fmt.Sprintf("request returned with http code=%d", code), e)
60+
}
61+
}
62+
63+
projectConfig, err := datafileProjectConfig.NewDatafileProjectConfig(payload)
64+
if err != nil {
65+
cm.metrics.Inc("failed_project_config")
66+
cmLogger.Error("failed to create project config", err)
67+
}
68+
69+
cm.configLock.Lock()
70+
cm.projectConfig = projectConfig
71+
cm.configLock.Unlock()
72+
}
73+
74+
if init {
75+
update()
76+
return
77+
}
78+
t := time.NewTicker(cm.pollingWait)
79+
for {
80+
select {
81+
case <-t.C:
82+
update()
83+
cm.metrics.Inc("polls")
84+
case <-cm.ctx.Done():
85+
cmLogger.Debug("Polling Config Manager Stopped")
86+
return
87+
}
88+
}
89+
}
90+
91+
func NewPollingProjectConfigManager(ctx context.Context, requester *Requester, initialPayload []byte, pollingWait time.Duration) *PollingProjectConfigManager {
92+
93+
if pollingWait == 0 {
94+
pollingWait = defaultPollingWait
95+
}
96+
97+
pollingProjectConfigManager := PollingProjectConfigManager{requester: requester, pollingWait: pollingWait, metrics: utils.NewMetrics(), ctx: ctx}
98+
99+
pollingProjectConfigManager.activate(initialPayload, true) // initial poll
100+
101+
cmLogger.Debug("Polling Config Manager Initiated")
102+
go pollingProjectConfigManager.activate([]byte{}, false)
103+
return &pollingProjectConfigManager
104+
}
105+
106+
// GetConfig returns the project config
107+
func (cm *PollingProjectConfigManager) GetConfig() optimizely.ProjectConfig {
108+
cm.configLock.RLock()
109+
defer cm.configLock.RUnlock()
110+
return cm.projectConfig
111+
}
112+
113+
//GetMetrics returns a string of all metrics
114+
func (cm *PollingProjectConfigManager) GetMetrics() string {
115+
return cm.metrics.String()
116+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/****************************************************************************
2+
* Copyright 2019, 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 config
18+
19+
import (
20+
"context"
21+
"log"
22+
"testing"
23+
"time"
24+
25+
"github.com/optimizely/go-sdk/optimizely/config/datafileProjectConfig"
26+
"github.com/stretchr/testify/assert"
27+
)
28+
29+
func TestNewPollingProjectConfigManager(t *testing.T) {
30+
URL := "https://cdn.optimizely.com/datafiles/4SLpaJA1r1pgE6T2CoMs9q_bad.json"
31+
projectConfig, _ := datafileProjectConfig.NewDatafileProjectConfig([]byte{})
32+
request := NewRequester(URL)
33+
34+
// Bad SDK Key test
35+
configManager := NewPollingProjectConfigManager(context.Background(), request, []byte{}, 0)
36+
assert.Equal(t, projectConfig, configManager.GetConfig())
37+
assert.Equal(t, "[bad_http_request:1, failed_project_config:1]", configManager.GetMetrics())
38+
39+
// Good SDK Key test
40+
URL = "https://cdn.optimizely.com/datafiles/4SLpaJA1r1pgE6T2CoMs9q.json"
41+
request = NewRequester(URL)
42+
configManager = NewPollingProjectConfigManager(context.Background(), request, []byte{}, 0)
43+
newConfig := configManager.GetConfig()
44+
45+
assert.Equal(t, "", newConfig.GetAccountID())
46+
assert.Equal(t, 3, len(newConfig.GetAudienceMap()))
47+
assert.Equal(t, "", configManager.GetMetrics())
48+
49+
}
50+
51+
func TestPollingMetrics(t *testing.T) {
52+
URL := "https://cdn.optimizely.com/datafiles/4SLpaJA1r1pgE6T2CoMs9q.json"
53+
request := NewRequester(URL)
54+
55+
// Good SDK Key test -- number of polling
56+
ctx := context.Background()
57+
ctx, cancel := context.WithCancel(ctx)
58+
configManager := NewPollingProjectConfigManager(ctx, request, []byte{}, 5*time.Second)
59+
time.Sleep(16 * time.Second)
60+
cancel()
61+
log.Print("sleeping")
62+
time.Sleep(5 * time.Second) // should have picked up another poll, but it is cancelled
63+
assert.Equal(t, "[polls:3]", configManager.GetMetrics())
64+
65+
}

0 commit comments

Comments
 (0)