Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/client/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func (f OptimizelyFactory) Client(clientOptions ...OptionFunc) (*OptimizelyClien

// Initialize the default services with the execution context
if pollingConfigManager, ok := appClient.ConfigManager.(*config.PollingProjectConfigManager); ok {
pollingConfigManager.Start(appClient.executionCtx)
pollingConfigManager.Start(f.SDKKey, appClient.executionCtx)
}

if batchProcessor, ok := appClient.EventProcessor.(*event.BatchEventProcessor); ok {
Expand Down
27 changes: 15 additions & 12 deletions pkg/client/factory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,7 @@ package client

import (
"errors"
"fmt"
"log"
"net/http"
"net/http/httptest"
"testing"
"time"

Expand All @@ -32,8 +29,19 @@ import (
"github.com/optimizely/go-sdk/pkg/utils"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

type MockRequester struct {
utils.Requester
mock.Mock
}

func (m *MockRequester) Get(uri string, headers ...utils.Header) (response []byte, responseHeaders http.Header, code int, err error) {
args := m.Called(headers)
return args.Get(0).([]byte), args.Get(1).(http.Header), args.Int(2), args.Error(3)
}

type MockDispatcher struct {
Events []event.LogEvent
}
Expand Down Expand Up @@ -75,16 +83,11 @@ func TestClientWithPollingConfigManager(t *testing.T) {

func TestClientWithPollingConfigManagerRequester(t *testing.T) {

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Print(">request: ", r)
if r.URL.String() == "/good" {
fmt.Fprintln(w, "Hello, client")
}
}))

factory := OptimizelyFactory{}
requester := utils.NewHTTPRequester(ts.URL + "/good")
optimizelyClient, err := factory.Client(WithPollingConfigManagerRequester(requester, time.Minute, nil))
mockRequester := new(MockRequester)
mockRequester.On("Get", []utils.Header(nil)).Return([]byte(`{"revision":"42"}`), http.Header{}, http.StatusOK, nil)

optimizelyClient, err := factory.Client(WithPollingConfigManagerRequester(mockRequester, time.Minute, nil))
assert.NoError(t, err)
assert.NotNil(t, optimizelyClient.ConfigManager)
assert.NotNil(t, optimizelyClient.DecisionService)
Expand Down
52 changes: 25 additions & 27 deletions pkg/config/polling_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,24 +34,25 @@ import (
// DefaultPollingInterval sets default interval for polling manager
const DefaultPollingInterval = 5 * time.Minute // default to 5 minutes for polling

// DatafileURLTemplate is used to construct the endpoint for retrieving the datafile from the CDN
const DatafileURLTemplate = "https://cdn.optimizely.com/datafiles/%s.json"

// ModifiedSince header key for request
const ModifiedSince = "If-Modified-Since"

// LastModified header key for response
const LastModified = "Last-Modified"

// DatafileURLTemplate is used to construct the endpoint for retrieving the datafile from the CDN
const DatafileURLTemplate = "https://cdn.optimizely.com/datafiles/%s.json"

var cmLogger = logging.GetLogger("PollingConfigManager")

// PollingProjectConfigManager maintains a dynamic copy of the project config
type PollingProjectConfigManager struct {
requester utils.Requester
pollingInterval time.Duration
notificationCenter notification.Center
initDatafile []byte
lastModified string
requester utils.Requester
pollingInterval time.Duration
notificationCenter notification.Center
initDatafile []byte
lastModified string
datafileURLTemplate string

configLock sync.RWMutex
err error
Expand All @@ -61,21 +62,17 @@ type PollingProjectConfigManager struct {
// OptionFunc is a type to a proper func
type OptionFunc func(*PollingProjectConfigManager)

// DefaultRequester is an optional function, sets default requester based on a key.
func DefaultRequester(sdkKey string) OptionFunc {
// Requester is an optional function, sets a passed requester
func Requester(requester utils.Requester) OptionFunc {
return func(p *PollingProjectConfigManager) {

url := fmt.Sprintf(DatafileURLTemplate, sdkKey)
requester := utils.NewHTTPRequester(url)

p.requester = requester
}
}

// Requester is an optional function, sets a passed requester
func Requester(requester utils.Requester) OptionFunc {
// DatafileTemplate is an optional function, sets a passed datafile URL template
func DatafileTemplate(datafileTemplate string) OptionFunc {
return func(p *PollingProjectConfigManager) {
p.requester = requester
p.datafileURLTemplate = datafileTemplate
}
}

Expand All @@ -94,7 +91,7 @@ func InitialDatafile(datafile []byte) OptionFunc {
}

// SyncConfig gets current datafile and updates projectConfig
func (cm *PollingProjectConfigManager) SyncConfig(datafile []byte) {
func (cm *PollingProjectConfigManager) SyncConfig(sdkKey string, datafile []byte) {
var e error
var code int
var respHeaders http.Header
Expand All @@ -104,12 +101,13 @@ func (cm *PollingProjectConfigManager) SyncConfig(datafile []byte) {
cm.configLock.Unlock()
}

url := fmt.Sprintf(cm.datafileURLTemplate, sdkKey)
if len(datafile) == 0 {
if cm.lastModified != "" {
lastModifiedHeader := utils.Header{Name: ModifiedSince, Value: cm.lastModified}
datafile, respHeaders, code, e = cm.requester.Get(lastModifiedHeader)
datafile, respHeaders, code, e = cm.requester.Get(url, lastModifiedHeader)
} else {
datafile, respHeaders, code, e = cm.requester.Get()
datafile, respHeaders, code, e = cm.requester.Get(url)
}

if e != nil {
Expand Down Expand Up @@ -168,14 +166,14 @@ func (cm *PollingProjectConfigManager) SyncConfig(datafile []byte) {
}

// Start starts the polling
func (cm *PollingProjectConfigManager) Start(exeCtx utils.ExecutionCtx) {
func (cm *PollingProjectConfigManager) Start(sdkKey string, exeCtx utils.ExecutionCtx) {
go func() {
cmLogger.Debug("Polling Config Manager Initiated")
t := time.NewTicker(cm.pollingInterval)
for {
select {
case <-t.C:
cm.SyncConfig([]byte{})
cm.SyncConfig(sdkKey, []byte{})
case <-exeCtx.GetContext().Done():
cmLogger.Debug("Polling Config Manager Stopped")
return
Expand All @@ -186,20 +184,20 @@ func (cm *PollingProjectConfigManager) Start(exeCtx utils.ExecutionCtx) {

// NewPollingProjectConfigManager returns an instance of the polling config manager with the customized configuration
func NewPollingProjectConfigManager(sdkKey string, pollingMangerOptions ...OptionFunc) *PollingProjectConfigManager {
url := fmt.Sprintf(DatafileURLTemplate, sdkKey)

pollingProjectConfigManager := PollingProjectConfigManager{
notificationCenter: registry.GetNotificationCenter(sdkKey),
pollingInterval: DefaultPollingInterval,
requester: utils.NewHTTPRequester(url),
notificationCenter: registry.GetNotificationCenter(sdkKey),
pollingInterval: DefaultPollingInterval,
requester: utils.NewHTTPRequester(),
datafileURLTemplate: DatafileURLTemplate,
}

for _, opt := range pollingMangerOptions {
opt(&pollingProjectConfigManager)
}

initDatafile := pollingProjectConfigManager.initDatafile
pollingProjectConfigManager.SyncConfig(initDatafile) // initial poll
pollingProjectConfigManager.SyncConfig(sdkKey, initDatafile) // initial poll
return &pollingProjectConfigManager
}

Expand Down
57 changes: 26 additions & 31 deletions pkg/config/polling_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ type MockRequester struct {
mock.Mock
}

func (m *MockRequester) Get(headers ...utils.Header) (response []byte, responseHeaders http.Header, code int, err error) {
func (m *MockRequester) Get(uri string, headers ...utils.Header) (response []byte, responseHeaders http.Header, code int, err error) {
args := m.Called(headers)
return args.Get(0).([]byte), args.Get(1).(http.Header), args.Int(2), args.Error(3)
}
Expand All @@ -51,7 +51,7 @@ func TestNewPollingProjectConfigManagerWithOptions(t *testing.T) {

exeCtx := utils.NewCancelableExecutionCtx()
configManager := NewPollingProjectConfigManager(sdkKey, Requester(mockRequester))
configManager.Start(exeCtx)
configManager.Start(sdkKey, exeCtx)
mockRequester.AssertExpectations(t)

actual, err := configManager.GetConfig()
Expand All @@ -72,7 +72,7 @@ func TestNewPollingProjectConfigManagerWithNull(t *testing.T) {

exeCtx := utils.NewCancelableExecutionCtx()
configManager := NewPollingProjectConfigManager(sdkKey, Requester(mockRequester))
configManager.Start(exeCtx)
configManager.Start(sdkKey, exeCtx)
mockRequester.AssertExpectations(t)

_, err := configManager.GetConfig()
Expand All @@ -90,15 +90,15 @@ func TestNewPollingProjectConfigManagerWithSimilarDatafileRevisions(t *testing.T

exeCtx := utils.NewCancelableExecutionCtx()
configManager := NewPollingProjectConfigManager(sdkKey, Requester(mockRequester))
configManager.Start(exeCtx)
configManager.Start(sdkKey, exeCtx)
mockRequester.AssertExpectations(t)

actual, err := configManager.GetConfig()
assert.Nil(t, err)
assert.NotNil(t, actual)
assert.Equal(t, projectConfig1, actual)

configManager.SyncConfig(mockDatafile2)
configManager.SyncConfig(sdkKey, mockDatafile2)
actual, err = configManager.GetConfig()
assert.Equal(t, projectConfig1, actual)
}
Expand All @@ -118,7 +118,7 @@ func TestNewPollingProjectConfigManagerWithLastModifiedDates(t *testing.T) {

exeCtx := utils.NewCancelableExecutionCtx()
configManager := NewPollingProjectConfigManager(sdkKey, Requester(mockRequester))
configManager.Start(exeCtx)
configManager.Start(sdkKey, exeCtx)

// Fetch valid config
actual, err := configManager.GetConfig()
Expand All @@ -127,7 +127,7 @@ func TestNewPollingProjectConfigManagerWithLastModifiedDates(t *testing.T) {
assert.Equal(t, projectConfig1, actual)

// Sync and check no changes were made to the previous config because of 304 error code
configManager.SyncConfig([]byte{})
configManager.SyncConfig(sdkKey, []byte{})
actual, err = configManager.GetConfig()
assert.Nil(t, err)
assert.NotNil(t, actual)
Expand All @@ -148,15 +148,15 @@ func TestNewPollingProjectConfigManagerWithDifferentDatafileRevisions(t *testing

exeCtx := utils.NewCancelableExecutionCtx()
configManager := NewPollingProjectConfigManager(sdkKey, Requester(mockRequester))
configManager.Start(exeCtx)
configManager.Start(sdkKey, exeCtx)
mockRequester.AssertExpectations(t)

actual, err := configManager.GetConfig()
assert.Nil(t, err)
assert.NotNil(t, actual)
assert.Equal(t, projectConfig1, actual)

configManager.SyncConfig(mockDatafile2)
configManager.SyncConfig(sdkKey, mockDatafile2)
actual, err = configManager.GetConfig()
assert.Equal(t, projectConfig2, actual)
}
Expand All @@ -175,20 +175,20 @@ func TestNewPollingProjectConfigManagerWithErrorHandling(t *testing.T) {

exeCtx := utils.NewCancelableExecutionCtx()
configManager := NewPollingProjectConfigManager(sdkKey, Requester(mockRequester))
configManager.Start(exeCtx)
configManager.Start(sdkKey, exeCtx)
mockRequester.AssertExpectations(t)

actual, err := configManager.GetConfig() // polling for bad file
assert.NotNil(t, err)
assert.Nil(t, actual)
assert.Nil(t, projectConfig1)

configManager.SyncConfig(mockDatafile2) // polling for good file
configManager.SyncConfig(sdkKey, mockDatafile2) // polling for good file
actual, err = configManager.GetConfig()
assert.Nil(t, err)
assert.Equal(t, projectConfig2, actual)

configManager.SyncConfig(mockDatafile1) // polling for bad file, error not null but good project
configManager.SyncConfig(sdkKey, mockDatafile1) // polling for bad file, error not null but good project
actual, err = configManager.GetConfig()
assert.Nil(t, err)
assert.Equal(t, projectConfig2, actual)
Expand All @@ -206,7 +206,7 @@ func TestNewPollingProjectConfigManagerOnDecision(t *testing.T) {

exeCtx := utils.NewCancelableExecutionCtx()
configManager := NewPollingProjectConfigManager(sdkKey, Requester(mockRequester))
configManager.Start(exeCtx)
configManager.Start(sdkKey, exeCtx)

var numberOfCalls = 0
callback := func(notification notification.ProjectConfigUpdateNotification) {
Expand All @@ -219,7 +219,7 @@ func TestNewPollingProjectConfigManagerOnDecision(t *testing.T) {
assert.Nil(t, err)
assert.NotNil(t, actual)

configManager.SyncConfig(mockDatafile2)
configManager.SyncConfig(sdkKey, mockDatafile2)
actual, err = configManager.GetConfig()
assert.Nil(t, err)
assert.NotNil(t, actual)
Expand All @@ -234,37 +234,32 @@ func TestNewPollingProjectConfigManagerOnDecision(t *testing.T) {
assert.Nil(t, err)
}

func TestDefaultRequester(t *testing.T) {

sdkKey := "test_sdk_key"
DefaultRequester(sdkKey)
exeCtx := utils.NewCancelableExecutionCtx()
configManager := NewPollingProjectConfigManager(sdkKey, DefaultRequester(sdkKey))
configManager.Start(exeCtx)

requester := configManager.requester
assert.NotNil(t, requester)
assert.Equal(t, requester.String(), "{url: https://cdn.optimizely.com/datafiles/test_sdk_key.json, timeout: 5s, retries: 1}")
}

func TestPollingInterval(t *testing.T) {

sdkKey := "test_sdk_key"
DefaultRequester(sdkKey)

exeCtx := utils.NewCancelableExecutionCtx()
configManager := NewPollingProjectConfigManager(sdkKey, PollingInterval(5*time.Second))
configManager.Start(exeCtx)
configManager.Start(sdkKey, exeCtx)

assert.Equal(t, configManager.pollingInterval, 5*time.Second)
}

func TestInitialDatafile(t *testing.T) {

sdkKey := "test_sdk_key"
DefaultRequester(sdkKey)
exeCtx := utils.NewCancelableExecutionCtx()
configManager := NewPollingProjectConfigManager(sdkKey, InitialDatafile([]byte("test")))
configManager.Start(exeCtx)
configManager.Start(sdkKey, exeCtx)

assert.Equal(t, configManager.initDatafile, []byte("test"))
}

func TestDatafileTemplate(t *testing.T) {

sdkKey := "test_sdk_key"
datafileTemplate := "https://localhost/v1/%s.json"
configManager := NewPollingProjectConfigManager(sdkKey, DatafileTemplate(datafileTemplate))

assert.Equal(t, datafileTemplate, configManager.datafileURLTemplate)
}
6 changes: 4 additions & 2 deletions pkg/config/static_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ type StaticProjectConfigManager struct {
// NewStaticProjectConfigManagerFromURL returns new instance of StaticProjectConfigManager for URL
func NewStaticProjectConfigManagerFromURL(sdkKey string) (*StaticProjectConfigManager, error) {

requester := utils.NewHTTPRequester(fmt.Sprintf(DatafileURLTemplate, sdkKey))
datafile, _, code, e := requester.Get()
requester := utils.NewHTTPRequester()

url := fmt.Sprintf(DatafileURLTemplate, sdkKey)
datafile, _, code, e := requester.Get(url)
if e != nil {
cmLogger.Error(fmt.Sprintf("request returned with http code=%d", code), e)
return nil, e
Expand Down
4 changes: 2 additions & 2 deletions pkg/event/dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ type HTTPEventDispatcher struct {
// DispatchEvent dispatches event with callback
func (*HTTPEventDispatcher) DispatchEvent(event LogEvent) (bool, error) {

requester := utils.NewHTTPRequester(event.EndPoint)
_, _, code, err := requester.Post(event.Event)
requester := utils.NewHTTPRequester()
_, _, code, err := requester.Post(event.EndPoint, event.Event)

// also check response codes
// resp.StatusCode == 400 is an error
Expand Down
Loading