Skip to content

Commit 1351a3f

Browse files
feat: Add config to allow surpressing notification on launch (flag cache load) (thomaspoignant#2534)
* Change the existing `UpdateCache` method to `UpdateCacheAndNotify` * Add config to control whether to notify on application init * Add a method to the cacheManager interface to update cache without notification * Don't send notification on cache initialization if DisableNotificationOnInit is set to true * Update docs to refer to the newly available configuration * Fix golangci-lint errors * Make the wait longer to ensure that async call has completed This also aligns the interval with rest of the test code * Add mutext to prevent race condition * use wasNotifyCalled() instead of directly calling notifyCalled * DisableNotificationOnInit -> DisableNotifierOnInit * Reduce number of interface methods by exposing the internal parameter of `CacheManager` * Introduce an additional parameter to `retrieveFlagsAndUpdateCache` to indicate initialization * Add comment to `retrieveFlags` to indicate its functionality * Use mocks from testutils/mock instead of creating mocks within the test files * Remove unnecessary references to / declarations of UpdateCacheAndNotify * Fix README description to be more accurate about DisableNotifierOnInit * Reduce length of the line to comply with linter --------- Co-authored-by: Thomas Poignant <[email protected]>
1 parent a1955f0 commit 1351a3f

File tree

12 files changed

+288
-20
lines changed

12 files changed

+288
-20
lines changed

cmd/relayproxy/config/config.go

+8
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,14 @@ type Config struct {
175175
// Default: false
176176
EnablePollingJitter bool `mapstructure:"enablePollingJitter" koanf:"enablepollingjitter"`
177177

178+
// DisableNotifierOnInit (optional) set to true if you do not want to call any notifier
179+
// when the flags are loaded.
180+
// This is useful if you do not want a Slack/Webhook notification saying that
181+
// the flags have been added every time you start the application.
182+
// Default is set to false for backward compatibility.
183+
// Default: false
184+
DisableNotifierOnInit bool `mapstructure:"DisableNotifierOnInit" koanf:"DisableNotifierOnInit"`
185+
178186
// FileFormat (optional) is the format of the file to retrieve (available YAML, TOML and JSON)
179187
// Default: YAML
180188
FileFormat string `mapstructure:"fileFormat" koanf:"fileformat"`

cmd/relayproxy/service/gofeatureflag.go

+1
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ func NewGoFeatureFlagClient(
9292
DataExporter: exp,
9393
StartWithRetrieverError: proxyConf.StartWithRetrieverError,
9494
EnablePollingJitter: proxyConf.EnablePollingJitter,
95+
DisableNotifierOnInit: proxyConf.DisableNotifierOnInit,
9596
EvaluationContextEnrichment: proxyConf.EvaluationContextEnrichment,
9697
PersistentFlagConfigurationFile: proxyConf.PersistentFlagConfigurationFile,
9798
}

config.go

+8
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ type Config struct {
2828
// Default: false
2929
EnablePollingJitter bool
3030

31+
// DisableNotifierOnInit (optional) set to true if you do not want to call any notifier
32+
// when the flags are loaded.
33+
// This is useful if you do not want a Slack/Webhook notification saying that
34+
// the flags have been added every time you start the application.
35+
// Default is set to false for backward compatibility.
36+
// Default: false
37+
DisableNotifierOnInit bool
38+
3139
// Deprecated: Use LeveledLogger instead
3240
// Logger (optional) logger use by the library
3341
// Default: No log

feature_flag.go

+24-8
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ func New(config Config) (*GoFeatureFlag, error) {
102102
return nil, fmt.Errorf("impossible to initialize the retrievers, please check your configuration: %v", err)
103103
}
104104

105-
err = retrieveFlagsAndUpdateCache(goFF.config, goFF.cache, goFF.retrieverManager)
105+
err = retrieveFlagsAndUpdateCache(goFF.config, goFF.cache, goFF.retrieverManager, true)
106106
if err != nil {
107107
switch {
108108
case config.PersistentFlagConfigurationFile != "":
@@ -154,7 +154,7 @@ func retrievePersistentLocalDisk(ctx context.Context, config Config, goFF *GoFea
154154
return err
155155
}
156156
defer func() { _ = fallBackRetrieverManager.Shutdown(ctx) }()
157-
err = retrieveFlagsAndUpdateCache(goFF.config, goFF.cache, fallBackRetrieverManager)
157+
err = retrieveFlagsAndUpdateCache(goFF.config, goFF.cache, fallBackRetrieverManager, true)
158158
if err != nil {
159159
return err
160160
}
@@ -192,7 +192,7 @@ func (g *GoFeatureFlag) startFlagUpdaterDaemon() {
192192
select {
193193
case <-g.bgUpdater.ticker.C:
194194
if !g.IsOffline() {
195-
err := retrieveFlagsAndUpdateCache(g.config, g.cache, g.retrieverManager)
195+
err := retrieveFlagsAndUpdateCache(g.config, g.cache, g.retrieverManager, false)
196196
if err != nil {
197197
g.config.internalLogger.Error("Error while updating the cache.", slog.Any("error", err))
198198
}
@@ -203,8 +203,13 @@ func (g *GoFeatureFlag) startFlagUpdaterDaemon() {
203203
}
204204
}
205205

206-
// retrieveFlagsAndUpdateCache is called every X seconds to refresh the cache flag.
207-
func retrieveFlagsAndUpdateCache(config Config, cache cache.Manager, retrieverManager *retriever.Manager) error {
206+
// retreiveFlags is a function that will retrieve the flags from the retrievers,
207+
// merge them and convert them to the flag struct.
208+
func retreiveFlags(
209+
config Config,
210+
cache cache.Manager,
211+
retrieverManager *retriever.Manager,
212+
) (map[string]dto.DTO, error) {
208213
retrievers := retrieverManager.GetRetrievers()
209214
// Results is the type that will receive the results when calling
210215
// all the retrievers.
@@ -250,7 +255,7 @@ func retrieveFlagsAndUpdateCache(config Config, cache cache.Manager, retrieverMa
250255
retrieversResults := make([]map[string]dto.DTO, len(retrievers))
251256
for v := range resultsChan {
252257
if v.Error != nil {
253-
return v.Error
258+
return nil, v.Error
254259
}
255260
retrieversResults[v.Index] = v.Value
256261
}
@@ -262,8 +267,19 @@ func retrieveFlagsAndUpdateCache(config Config, cache cache.Manager, retrieverMa
262267
newFlags[flagName] = value
263268
}
264269
}
270+
return newFlags, nil
271+
}
272+
273+
// retrieveFlagsAndUpdateCache is a function that retrieves the flags from the retrievers,
274+
// and update the cache with the new flags.
275+
func retrieveFlagsAndUpdateCache(config Config, cache cache.Manager,
276+
retrieverManager *retriever.Manager, isInit bool) error {
277+
newFlags, err := retreiveFlags(config, cache, retrieverManager)
278+
if err != nil {
279+
return err
280+
}
265281

266-
err := cache.UpdateCache(newFlags, config.internalLogger)
282+
err = cache.UpdateCache(newFlags, config.internalLogger, isInit && !config.DisableNotifierOnInit)
267283
if err != nil {
268284
log.Printf("error: impossible to update the cache of the flags: %v", err)
269285
return err
@@ -286,7 +302,7 @@ func (g *GoFeatureFlag) ForceRefresh() bool {
286302
if g.IsOffline() {
287303
return false
288304
}
289-
err := retrieveFlagsAndUpdateCache(g.config, g.cache, g.retrieverManager)
305+
err := retrieveFlagsAndUpdateCache(g.config, g.cache, g.retrieverManager, false)
290306
if err != nil {
291307
g.config.internalLogger.Error("Error while force updating the cache.", slog.Any("error", err))
292308
return false

feature_flag_test.go

+51
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/thomaspoignant/go-feature-flag/ffcontext"
1515
"github.com/thomaspoignant/go-feature-flag/internal/flag"
1616
"github.com/thomaspoignant/go-feature-flag/model"
17+
"github.com/thomaspoignant/go-feature-flag/notifier"
1718
"github.com/thomaspoignant/go-feature-flag/retriever"
1819
"github.com/thomaspoignant/go-feature-flag/retriever/fileretriever"
1920
"github.com/thomaspoignant/go-feature-flag/retriever/s3retriever"
@@ -706,3 +707,53 @@ func Test_UseCustomBucketingKey(t *testing.T) {
706707
assert.Equal(t, want, got)
707708
}
708709
}
710+
711+
func Test_DisableNotifierOnInit(t *testing.T) {
712+
tests := []struct {
713+
name string
714+
config *ffclient.Config
715+
disableNotification bool
716+
expectedNotifyCalled bool
717+
}{
718+
{
719+
name: "DisableNotifierOnInit is true",
720+
config: &ffclient.Config{
721+
PollingInterval: 60 * time.Second,
722+
Retriever: &fileretriever.Retriever{Path: "testdata/flag-config.yaml"},
723+
DisableNotifierOnInit: true,
724+
},
725+
expectedNotifyCalled: false,
726+
},
727+
{
728+
name: "DisableNotifierOnInit is false",
729+
config: &ffclient.Config{
730+
PollingInterval: 60 * time.Second,
731+
Retriever: &fileretriever.Retriever{Path: "testdata/flag-config.yaml"},
732+
DisableNotifierOnInit: false,
733+
},
734+
expectedNotifyCalled: true,
735+
},
736+
{
737+
name: "DisableNotifierOnInit is not set",
738+
config: &ffclient.Config{
739+
PollingInterval: 60 * time.Second,
740+
Retriever: &fileretriever.Retriever{Path: "testdata/flag-config.yaml"},
741+
},
742+
expectedNotifyCalled: true,
743+
},
744+
}
745+
746+
for _, tt := range tests {
747+
t.Run(tt.name, func(t *testing.T) {
748+
mockNotifier := &mock.Notifier{}
749+
tt.config.Notifiers = []notifier.Notifier{mockNotifier}
750+
751+
gffClient, err := ffclient.New(*tt.config)
752+
assert.NoError(t, err)
753+
defer gffClient.Close()
754+
755+
time.Sleep(2 * time.Second) // wait for the goroutine to call Notify()
756+
assert.Equal(t, tt.expectedNotifyCalled, mockNotifier.GetNotifyCalls() > 0)
757+
})
758+
}
759+
}

internal/cache/cache_manager.go

+6-4
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import (
1919

2020
type Manager interface {
2121
ConvertToFlagStruct(loadedFlags []byte, fileFormat string) (map[string]dto.DTO, error)
22-
UpdateCache(newFlags map[string]dto.DTO, log *fflog.FFLogger) error
22+
UpdateCache(newFlags map[string]dto.DTO, log *fflog.FFLogger, notifyChanges bool) error
2323
Close()
2424
GetFlag(key string) (flag.Flag, error)
2525
AllFlags() (map[string]flag.Flag, error)
@@ -60,7 +60,7 @@ func (c *cacheManagerImpl) ConvertToFlagStruct(loadedFlags []byte, fileFormat st
6060
return newFlags, err
6161
}
6262

63-
func (c *cacheManagerImpl) UpdateCache(newFlags map[string]dto.DTO, log *fflog.FFLogger) error {
63+
func (c *cacheManagerImpl) UpdateCache(newFlags map[string]dto.DTO, log *fflog.FFLogger, notifyChanges bool) error {
6464
newCache := NewInMemoryCache(c.logger)
6565
newCache.Init(newFlags)
6666
newCacheFlags := newCache.All()
@@ -75,8 +75,10 @@ func (c *cacheManagerImpl) UpdateCache(newFlags map[string]dto.DTO, log *fflog.F
7575
c.latestUpdate = time.Now()
7676
c.mutex.Unlock()
7777

78-
// notify the changes
79-
c.notificationService.Notify(oldCacheFlags, newCacheFlags, log)
78+
if notifyChanges {
79+
// notify the changes
80+
c.notificationService.Notify(oldCacheFlags, newCacheFlags, log)
81+
}
8082
// persist the cache on disk
8183
if c.persistentFlagConfigurationFile != "" {
8284
c.PersistCache(oldCacheFlags, newCacheFlags)

0 commit comments

Comments
 (0)