Skip to content

Commit 9065f67

Browse files
author
Michael Ng
authored
refac(config-manager): simplifies the creation of polling config manager with options (#61)
1 parent 183ff36 commit 9065f67

File tree

5 files changed

+102
-62
lines changed

5 files changed

+102
-62
lines changed

optimizely/client/factory.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@ import (
2828
"github.com/optimizely/go-sdk/optimizely/decision"
2929
)
3030

31-
const datafileURLTemplate = "https://cdn.optimizely.com/datafiles/%s.json"
32-
3331
// Options are used to create an instance of the OptimizelyClient with custom configuration
3432
type Options struct {
3533
Context context.Context
@@ -47,7 +45,7 @@ func (f OptimizelyFactory) StaticClient() (*OptimizelyClient, error) {
4745
var configManager optimizely.ProjectConfigManager
4846

4947
if f.SDKKey != "" {
50-
url := fmt.Sprintf(datafileURLTemplate, f.SDKKey)
48+
url := fmt.Sprintf(config.DatafileURLTemplate, f.SDKKey)
5149
staticConfigManager, err := config.NewStaticProjectConfigManagerFromURL(url)
5250

5351
if err != nil {
@@ -95,9 +93,10 @@ func (f OptimizelyFactory) ClientWithOptions(clientOptions Options) (*Optimizely
9593
if clientOptions.ProjectConfigManager != nil {
9694
client.configManager = clientOptions.ProjectConfigManager
9795
} else if f.SDKKey != "" {
98-
url := fmt.Sprintf(datafileURLTemplate, f.SDKKey)
99-
request := config.NewRequester(url)
100-
client.configManager = config.NewPollingProjectConfigManager(ctx, request, f.Datafile, 0)
96+
options := config.PollingProjectConfigManagerOptions{
97+
Datafile: f.Datafile,
98+
}
99+
client.configManager = config.NewPollingProjectConfigManagerWithOptions(ctx, f.SDKKey, options)
101100
} else if f.Datafile != nil {
102101
staticConfigManager, _ := config.NewStaticProjectConfigManagerFromPayload(f.Datafile)
103102
client.configManager = staticConfigManager

optimizely/config/polling_manager.go

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,26 @@ import (
2727
"github.com/optimizely/go-sdk/optimizely/logging"
2828
)
2929

30-
const defaultPollingWait = time.Duration(5 * time.Minute) // default 5 minutes for polling wait
30+
const defaultPollingInterval = time.Duration(5 * time.Minute) // default to 5 minutes for polling
31+
32+
// DatafileURLTemplate is used to construct the endpoint for retrieving the datafile from the CDN
33+
const DatafileURLTemplate = "https://cdn.optimizely.com/datafiles/%s.json"
3134

3235
var cmLogger = logging.GetLogger("PollingConfigManager")
3336

37+
// PollingProjectConfigManagerOptions used to create an instance with custom configuration
38+
type PollingProjectConfigManagerOptions struct {
39+
Datafile []byte
40+
PollingInterval time.Duration
41+
Requester Requester
42+
}
43+
3444
// PollingProjectConfigManager maintains a dynamic copy of the project config
3545
type PollingProjectConfigManager struct {
36-
requester *Requester
37-
pollingWait time.Duration
38-
projectConfig optimizely.ProjectConfig
39-
configLock sync.RWMutex
46+
requester Requester
47+
pollingInterval time.Duration
48+
projectConfig optimizely.ProjectConfig
49+
configLock sync.RWMutex
4050

4151
ctx context.Context // context used for cancellation
4252
}
@@ -71,7 +81,7 @@ func (cm *PollingProjectConfigManager) activate(initialPayload []byte, init bool
7181
update()
7282
return
7383
}
74-
t := time.NewTicker(cm.pollingWait)
84+
t := time.NewTicker(cm.pollingInterval)
7585
for {
7686
select {
7787
case <-t.C:
@@ -83,22 +93,38 @@ func (cm *PollingProjectConfigManager) activate(initialPayload []byte, init bool
8393
}
8494
}
8595

86-
// NewPollingProjectConfigManager returns new instance of PollingProjectConfigManager
87-
func NewPollingProjectConfigManager(ctx context.Context, requester *Requester, initialPayload []byte, pollingWait time.Duration) *PollingProjectConfigManager {
96+
// NewPollingProjectConfigManagerWithOptions returns new instance of PollingProjectConfigManager with the given options
97+
func NewPollingProjectConfigManagerWithOptions(ctx context.Context, sdkKey string, options PollingProjectConfigManagerOptions) *PollingProjectConfigManager {
8898

89-
if pollingWait == 0 {
90-
pollingWait = defaultPollingWait
99+
var requester Requester
100+
if options.Requester != nil {
101+
requester = options.Requester
102+
} else {
103+
url := fmt.Sprintf(DatafileURLTemplate, sdkKey)
104+
requester = NewHTTPRequester(url)
91105
}
92106

93-
pollingProjectConfigManager := PollingProjectConfigManager{requester: requester, pollingWait: pollingWait, ctx: ctx}
107+
pollingInterval := options.PollingInterval
108+
if pollingInterval == 0 {
109+
pollingInterval = defaultPollingInterval
110+
}
94111

95-
pollingProjectConfigManager.activate(initialPayload, true) // initial poll
112+
pollingProjectConfigManager := PollingProjectConfigManager{requester: requester, pollingInterval: pollingInterval, ctx: ctx}
113+
114+
pollingProjectConfigManager.activate(options.Datafile, true) // initial poll
96115

97116
cmLogger.Debug("Polling Config Manager Initiated")
98117
go pollingProjectConfigManager.activate([]byte{}, false)
99118
return &pollingProjectConfigManager
100119
}
101120

121+
// NewPollingProjectConfigManager returns an instance of the polling config manager with the default configuration
122+
func NewPollingProjectConfigManager(ctx context.Context, sdkKey string) *PollingProjectConfigManager {
123+
options := PollingProjectConfigManagerOptions{}
124+
configManager := NewPollingProjectConfigManagerWithOptions(ctx, sdkKey, options)
125+
return configManager
126+
}
127+
102128
// GetConfig returns the project config
103129
func (cm *PollingProjectConfigManager) GetConfig() optimizely.ProjectConfig {
104130
cm.configLock.RLock()

optimizely/config/polling_manager_test.go

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,34 @@ import (
2020
"context"
2121
"testing"
2222

23-
"github.com/optimizely/go-sdk/optimizely/config/datafileprojectconfig"
2423
"github.com/stretchr/testify/assert"
24+
25+
"github.com/optimizely/go-sdk/optimizely/config/datafileprojectconfig"
26+
"github.com/stretchr/testify/mock"
2527
)
2628

27-
func TestNewPollingProjectConfigManager(t *testing.T) {
28-
URL := "https://cdn.optimizely.com/datafiles/4SLpaJA1r1pgE6T2CoMs9q_bad.json"
29-
projectConfig, _ := datafileprojectconfig.NewDatafileProjectConfig([]byte{})
30-
request := NewRequester(URL)
29+
type MockRequester struct {
30+
Requester
31+
mock.Mock
32+
}
3133

32-
// Bad SDK Key test
33-
configManager := NewPollingProjectConfigManager(context.Background(), request, []byte{}, 0)
34-
assert.Equal(t, projectConfig, configManager.GetConfig())
34+
func (m *MockRequester) Get(headers ...Header) (response []byte, code int, err error) {
35+
args := m.Called(headers)
36+
return args.Get(0).([]byte), args.Int(1), args.Error(2)
37+
}
3538

36-
// Good SDK Key test
37-
URL = "https://cdn.optimizely.com/datafiles/4SLpaJA1r1pgE6T2CoMs9q.json"
38-
request = NewRequester(URL)
39-
configManager = NewPollingProjectConfigManager(context.Background(), request, []byte{}, 0)
40-
newConfig := configManager.GetConfig()
39+
func TestNewPollingProjectConfigManagerWithOptions(t *testing.T) {
40+
mockDatafile := []byte("{ revision: \"42\" }")
41+
projectConfig, _ := datafileprojectconfig.NewDatafileProjectConfig(mockDatafile)
42+
mockRequester := new(MockRequester)
43+
mockRequester.On("Get", []Header(nil)).Return(mockDatafile, 200, nil)
4144

42-
assert.Equal(t, "", newConfig.GetAccountID())
43-
assert.Equal(t, 4, len(newConfig.GetAudienceMap()))
45+
// Test we fetch using requester
46+
sdkKey := "test_sdk_key"
47+
options := PollingProjectConfigManagerOptions{
48+
Requester: mockRequester,
49+
}
50+
configManager := NewPollingProjectConfigManagerWithOptions(context.Background(), sdkKey, options)
51+
mockRequester.AssertExpectations(t)
52+
assert.Equal(t, projectConfig, configManager.GetConfig())
4453
}

optimizely/config/requester.go

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,47 +32,53 @@ const defaultTTL = 5 * time.Second
3232

3333
var requesterLogger = logging.GetLogger("Requester")
3434

35+
// Requester is used to make outbound requests with
36+
type Requester interface {
37+
Get(...Header) (response []byte, code int, err error)
38+
GetObj(result interface{}, headers ...Header) error
39+
}
40+
3541
// Header element to be sent
3642
type Header struct {
3743
Name, Value string
3844
}
3945

4046
// Timeout sets http client timeout
41-
func Timeout(timeout time.Duration) func(r *Requester) {
42-
return func(r *Requester) {
47+
func Timeout(timeout time.Duration) func(r *HTTPRequester) {
48+
return func(r *HTTPRequester) {
4349
r.client = http.Client{Timeout: timeout}
4450
}
4551
}
4652

4753
// Retries sets max number of retries for failed calls
48-
func Retries(retries int) func(r *Requester) {
49-
return func(r *Requester) {
54+
func Retries(retries int) func(r *HTTPRequester) {
55+
return func(r *HTTPRequester) {
5056
r.retries = retries
5157
}
5258
}
5359

5460
// Headers sets request headers
55-
func Headers(headers ...Header) func(r *Requester) {
56-
return func(r *Requester) {
61+
func Headers(headers ...Header) func(r *HTTPRequester) {
62+
return func(r *HTTPRequester) {
5763
r.headers = []Header{}
5864
r.headers = append(r.headers, headers...)
5965
}
6066
}
6167

62-
// Requester contains main info
63-
type Requester struct {
68+
// HTTPRequester contains main info
69+
type HTTPRequester struct {
6470
url string
6571
client http.Client
6672
retries int
6773
headers []Header
6874
ttl time.Duration // time-to-live
6975
}
7076

71-
// NewRequester makes Requester with api and parameters. Sets defaults
77+
// NewHTTPRequester makes Requester with api and parameters. Sets defaults
7278
// url has a complete url of the request like https://cdn.optimizely.com/datafiles/24234.json
73-
func NewRequester(url string, params ...func(*Requester)) *Requester {
79+
func NewHTTPRequester(url string, params ...func(*HTTPRequester)) *HTTPRequester {
7480

75-
res := Requester{
81+
res := HTTPRequester{
7682
url: url,
7783
retries: 1,
7884
headers: []Header{{"Content-Type", "application/json"}, {"Accept", "application/json"}},
@@ -87,12 +93,12 @@ func NewRequester(url string, params ...func(*Requester)) *Requester {
8793

8894
// Get executes HTTP GET with uri and optional extra headers, returns body in []bytes
8995
// url created as url+sdkKey.json
90-
func (r Requester) Get(headers ...Header) (response []byte, code int, err error) {
96+
func (r HTTPRequester) Get(headers ...Header) (response []byte, code int, err error) {
9197
return r.Do("GET", headers)
9298
}
9399

94100
// GetObj executes HTTP GET with uri and optional extra headers, returns filled object
95-
func (r Requester) GetObj(result interface{}, headers ...Header) error {
101+
func (r HTTPRequester) GetObj(result interface{}, headers ...Header) error {
96102
b, _, err := r.Do("GET", headers)
97103
if err != nil {
98104
return err
@@ -101,7 +107,7 @@ func (r Requester) GetObj(result interface{}, headers ...Header) error {
101107
}
102108

103109
// Do executes request and returns response body for requested uri (sdkKey.json).
104-
func (r Requester) Do(method string, headers []Header) (response []byte, code int, err error) {
110+
func (r HTTPRequester) Do(method string, headers []Header) (response []byte, code int, err error) {
105111

106112
single := func(request *http.Request) (response []byte, code int, e error) {
107113
resp, doErr := r.client.Do(request)
@@ -159,7 +165,7 @@ func (r Requester) Do(method string, headers []Header) (response []byte, code in
159165
return response, code, err
160166
}
161167

162-
func (r Requester) addHeaders(req *http.Request, headers []Header) *http.Request {
168+
func (r HTTPRequester) addHeaders(req *http.Request, headers []Header) *http.Request {
163169
for _, h := range r.headers {
164170
req.Header.Add(h.Name, h.Value)
165171
}
@@ -169,7 +175,7 @@ func (r Requester) addHeaders(req *http.Request, headers []Header) *http.Request
169175
return req
170176
}
171177

172-
func (r Requester) String() string {
178+
func (r HTTPRequester) String() string {
173179
return fmt.Sprintf("{url: %s, timeout: %v, retries: %d}",
174180
r.url, r.client.Timeout, r.retries)
175181
}

optimizely/config/requester_test.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,13 @@ func TestGet(t *testing.T) {
4242
}))
4343
defer ts.Close()
4444

45-
var httpreq *Requester
46-
httpreq = NewRequester(ts.URL + "/good")
45+
var httpreq Requester
46+
httpreq = NewHTTPRequester(ts.URL + "/good")
4747
resp, code, err := httpreq.Get()
4848
assert.Nil(t, err)
4949
assert.Equal(t, "Hello, client\n", string(resp))
5050

51-
httpreq = NewRequester(ts.URL + "/bad")
51+
httpreq = NewHTTPRequester(ts.URL + "/bad")
5252
_, code, err = httpreq.Get()
5353
assert.Equal(t, errors.New("400 Bad Request"), err)
5454
assert.Equal(t, code, 400)
@@ -72,16 +72,16 @@ func TestGetObj(t *testing.T) {
7272
}))
7373
defer ts.Close()
7474

75-
var httpreq *Requester
76-
httpreq = NewRequester(ts.URL + "/good")
75+
var httpreq Requester
76+
httpreq = NewHTTPRequester(ts.URL + "/good")
7777
r := resp{}
7878
err := httpreq.GetObj(&r)
7979
assert.Nil(t, err)
8080
assert.Equal(t, resp{Fld1: "Hello, client", Fld2: 123}, r)
8181
}
8282

8383
func TestGetBad(t *testing.T) {
84-
httpreq := NewRequester("blah12345/good")
84+
httpreq := NewHTTPRequester("blah12345/good")
8585
_, _, err := httpreq.Get()
8686
_, ok := err.(*url.Error)
8787
assert.True(t, ok, "url error")
@@ -101,7 +101,7 @@ func TestGetBadWithResponse(t *testing.T) {
101101
}))
102102
defer ts.Close()
103103

104-
httpreq := NewRequester(ts.URL+"/bad", Retries(1))
104+
httpreq := NewHTTPRequester(ts.URL+"/bad", Retries(1))
105105
data, _, err := httpreq.Get()
106106
assert.Equal(t, "400 Bad Request", err.Error())
107107
assert.Equal(t, "bad bad response\n", string(data))
@@ -130,7 +130,7 @@ func TestGetRetry(t *testing.T) {
130130
}))
131131
defer ts.Close()
132132

133-
httpreq := NewRequester(ts.URL+"/test", Retries(10))
133+
httpreq := NewHTTPRequester(ts.URL+"/test", Retries(10))
134134

135135
st := time.Now()
136136
resp, _, err := httpreq.Get()
@@ -141,27 +141,27 @@ func TestGetRetry(t *testing.T) {
141141

142142
assert.True(t, elapsed >= 400*5*time.Millisecond && elapsed <= 510*5*time.Second, "took %s", elapsed)
143143

144-
httpreq = NewRequester(ts.URL+"/test", Retries(3))
144+
httpreq = NewHTTPRequester(ts.URL+"/test", Retries(3))
145145
called = 0
146146
_, _, err = httpreq.Get()
147147
assert.Equal(t, errors.New("400 Bad Request"), err)
148148
assert.Equal(t, 3, called, "called 3 retries")
149149

150-
httpreq = NewRequester(ts.URL+"/test", Retries(1))
150+
httpreq = NewHTTPRequester(ts.URL+"/test", Retries(1))
151151
called = 0
152152
_, _, err = httpreq.Get()
153153
assert.Equal(t, errors.New("400 Bad Request"), err)
154154
assert.Equal(t, 1, called, "called 1 retries")
155155

156-
httpreq = NewRequester(ts.URL + "/test")
156+
httpreq = NewHTTPRequester(ts.URL + "/test")
157157
called = 0
158158
_, _, err = httpreq.Get()
159159
assert.Equal(t, errors.New("400 Bad Request"), err)
160160
assert.Equal(t, 1, called, "called 1 retries")
161161
}
162162

163163
func TestString(t *testing.T) {
164-
assert.Equal(t, "{url: 127.0.0.1/blah, timeout: 5s, retries: 1}", NewRequester("127.0.0.1/blah").String())
164+
assert.Equal(t, "{url: 127.0.0.1/blah, timeout: 5s, retries: 1}", NewHTTPRequester("127.0.0.1/blah").String())
165165
assert.Equal(t, "{url: 127.0.0.1/blah, timeout: 19s, retries: 10}",
166-
NewRequester("127.0.0.1/blah", Retries(10), Timeout(time.Duration(19)*time.Second)).String())
166+
NewHTTPRequester("127.0.0.1/blah", Retries(10), Timeout(time.Duration(19)*time.Second)).String())
167167
}

0 commit comments

Comments
 (0)