Skip to content

Commit 36323cd

Browse files
Merge pull request #114 from optimizely/pawel/OASIS-5262
Added project config update notification
2 parents a142f46 + 96b0007 commit 36323cd

File tree

5 files changed

+105
-14
lines changed

5 files changed

+105
-14
lines changed

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,7 @@ require (
99
github.com/stretchr/testify v1.4.0
1010
github.com/twmb/murmur3 v1.0.0
1111
)
12+
13+
14+
// Work around issue wtih git.apache.org/thrift.git
15+
replace git.apache.org/thrift.git => github.com/apache/thrift v0.12.0

optimizely/config/polling_manager.go

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/optimizely/go-sdk/optimizely"
2626
"github.com/optimizely/go-sdk/optimizely/config/datafileprojectconfig"
2727
"github.com/optimizely/go-sdk/optimizely/logging"
28+
"github.com/optimizely/go-sdk/optimizely/notification"
2829
"github.com/optimizely/go-sdk/optimizely/utils"
2930
)
3031

@@ -37,18 +38,20 @@ var cmLogger = logging.GetLogger("PollingConfigManager")
3738

3839
// PollingProjectConfigManagerOptions used to create an instance with custom configuration
3940
type PollingProjectConfigManagerOptions struct {
40-
Datafile []byte
41-
PollingInterval time.Duration
42-
Requester utils.Requester
41+
Datafile []byte
42+
PollingInterval time.Duration
43+
Requester utils.Requester
44+
NotificationCenter notification.Center
4345
}
4446

4547
// PollingProjectConfigManager maintains a dynamic copy of the project config
4648
type PollingProjectConfigManager struct {
47-
requester utils.Requester
48-
pollingInterval time.Duration
49-
projectConfig optimizely.ProjectConfig
50-
configLock sync.RWMutex
51-
err error
49+
requester utils.Requester
50+
pollingInterval time.Duration
51+
projectConfig optimizely.ProjectConfig
52+
configLock sync.RWMutex
53+
err error
54+
notificationCenter notification.Center
5255

5356
exeCtx utils.ExecutionCtx // context used for execution control
5457
}
@@ -77,6 +80,16 @@ func (cm *PollingProjectConfigManager) SyncConfig(datafile []byte) {
7780
} else {
7881
cmLogger.Debug(fmt.Sprintf("Received new datafile and updated config. Old revision number: %s. New revision number: %s", cm.projectConfig.GetRevision(), projectConfig.GetRevision()))
7982
cm.projectConfig = projectConfig
83+
84+
if cm.notificationCenter != nil {
85+
projectConfigUpdateNotification := notification.ProjectConfigUpdateNotification{
86+
Type: notification.ProjectConfigUpdate,
87+
Revision: cm.projectConfig.GetRevision(),
88+
}
89+
if err = cm.notificationCenter.Send(notification.ProjectConfigUpdate, projectConfigUpdateNotification); err != nil {
90+
cmLogger.Warning("Problem with sending notification")
91+
}
92+
}
8093
}
8194
} else {
8295
cm.projectConfig = projectConfig
@@ -120,7 +133,7 @@ func NewPollingProjectConfigManagerWithOptions(exeCtx utils.ExecutionCtx, sdkKey
120133
pollingInterval = defaultPollingInterval
121134
}
122135

123-
pollingProjectConfigManager := PollingProjectConfigManager{requester: requester, pollingInterval: pollingInterval, exeCtx: exeCtx}
136+
pollingProjectConfigManager := PollingProjectConfigManager{requester: requester, pollingInterval: pollingInterval, notificationCenter: options.NotificationCenter, exeCtx: exeCtx}
124137

125138
pollingProjectConfigManager.SyncConfig(options.Datafile) // initial poll
126139

@@ -142,3 +155,29 @@ func (cm *PollingProjectConfigManager) GetConfig() (optimizely.ProjectConfig, er
142155
defer cm.configLock.RUnlock()
143156
return cm.projectConfig, cm.err
144157
}
158+
159+
// OnProjectConfigUpdate registers a handler for ProjectConfigUpdate notifications
160+
func (cm *PollingProjectConfigManager) OnProjectConfigUpdate(callback func(notification.ProjectConfigUpdateNotification)) (int, error) {
161+
handler := func(payload interface{}) {
162+
if projectConfigUpdateNotification, ok := payload.(notification.ProjectConfigUpdateNotification); ok {
163+
callback(projectConfigUpdateNotification)
164+
} else {
165+
cmLogger.Warning(fmt.Sprintf("Unable to convert notification payload %v into ProjectConfigUpdateNotification", payload))
166+
}
167+
}
168+
id, err := cm.notificationCenter.AddHandler(notification.ProjectConfigUpdate, handler)
169+
if err != nil {
170+
cmLogger.Warning("Problem with adding notification handler")
171+
return 0, err
172+
}
173+
return id, nil
174+
}
175+
176+
// RemoveOnProjectConfigUpdate removes handler for ProjectConfigUpdate notification with given id
177+
func (cm *PollingProjectConfigManager) RemoveOnProjectConfigUpdate(id int) error {
178+
if err := cm.notificationCenter.RemoveHandler(id, notification.ProjectConfigUpdate); err != nil {
179+
cmLogger.Warning("Problem with removing notification handler")
180+
return err
181+
}
182+
return nil
183+
}

optimizely/config/polling_manager_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"testing"
2121

2222
"github.com/optimizely/go-sdk/optimizely/config/datafileprojectconfig"
23+
"github.com/optimizely/go-sdk/optimizely/notification"
2324
"github.com/optimizely/go-sdk/optimizely/utils"
2425

2526
"github.com/stretchr/testify/assert"
@@ -130,3 +131,43 @@ func TestNewPollingProjectConfigManagerWithDifferentDatafileRevisions(t *testing
130131
actual, err = configManager.GetConfig()
131132
assert.Equal(t, projectConfig2, actual)
132133
}
134+
135+
func TestNewPollingProjectConfigManagerOnDecision(t *testing.T) {
136+
mockDatafile1 := []byte(`{"revision":"42","botFiltering":true}`)
137+
mockDatafile2 := []byte(`{"revision":"43","botFiltering":false}`)
138+
139+
mockRequester := new(MockRequester)
140+
mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile1, 200, nil)
141+
142+
// Test we fetch using requester
143+
sdkKey := "test_sdk_key"
144+
options := PollingProjectConfigManagerOptions{
145+
Requester: mockRequester,
146+
NotificationCenter: notification.NewNotificationCenter(),
147+
}
148+
149+
exeCtx := utils.NewCancelableExecutionCtx()
150+
configManager := NewPollingProjectConfigManagerWithOptions(exeCtx, sdkKey, options)
151+
152+
var numberOfCalls = 0
153+
callback := func(notification notification.ProjectConfigUpdateNotification) {
154+
numberOfCalls++
155+
}
156+
id, _ := configManager.OnProjectConfigUpdate(callback)
157+
mockRequester.AssertExpectations(t)
158+
159+
actual, err := configManager.GetConfig()
160+
assert.Nil(t, err)
161+
assert.NotNil(t, actual)
162+
163+
configManager.SyncConfig(mockDatafile2)
164+
actual, err = configManager.GetConfig()
165+
assert.Nil(t, err)
166+
assert.NotNil(t, actual)
167+
168+
assert.NotEqual(t, id, 0)
169+
assert.Equal(t, numberOfCalls, 1)
170+
171+
err = configManager.RemoveOnProjectConfigUpdate(id)
172+
assert.Nil(t, err)
173+
}

optimizely/notification/center.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ type DefaultCenter struct {
3434
// NewNotificationCenter returns a new notification center
3535
func NewNotificationCenter() *DefaultCenter {
3636
decisionNotificationManager := NewAtomicManager()
37+
projectConfigUpdateNotificationManager := NewAtomicManager()
3738
managerMap := make(map[Type]Manager)
3839
managerMap[Decision] = decisionNotificationManager
40+
managerMap[ProjectConfigUpdate] = projectConfigUpdateNotificationManager
3941
return &DefaultCenter{
4042
managerMap: managerMap,
4143
}

optimizely/notification/entities.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,14 @@ import "github.com/optimizely/go-sdk/optimizely/entities"
2222
// Type is the type of notification
2323
type Type string
2424

25-
const (
26-
// Decision notification type
27-
Decision Type = "decision"
28-
)
29-
3025
// DecisionNotificationType is the type of decision notification
3126
type DecisionNotificationType string
3227

3328
const (
29+
// Decision notification type
30+
Decision Type = "decision"
31+
// ProjectConfigUpdate notification type
32+
ProjectConfigUpdate Type = "project_config_update"
3433
// Feature is used when the decision is returned as part of evaluating a feature
3534
Feature DecisionNotificationType = "feature"
3635
)
@@ -41,3 +40,9 @@ type DecisionNotification struct {
4140
UserContext entities.UserContext
4241
DecisionInfo map[string]interface{}
4342
}
43+
44+
// ProjectConfigUpdateNotification is a notification triggered when a project config is updated
45+
type ProjectConfigUpdateNotification struct {
46+
Type Type
47+
Revision string
48+
}

0 commit comments

Comments
 (0)