Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add config to allow surpressing notification on launch (flag cache load) #2534

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
8 changes: 8 additions & 0 deletions cmd/relayproxy/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,14 @@ type Config struct {
// Default: false
EnablePollingJitter bool `mapstructure:"enablePollingJitter" koanf:"enablepollingjitter"`

// DisableNotificationOnInit (optional) set to true if you do not want to
// send a notification when the flags are loaded.
// This is useful if you do not want a Slack/Webhook notification saying that
// the flags have been added every time you start the application.
// Default is set to false for backward compatibility.
// Default: false
DisableNotificationOnInit bool `mapstructure:"disableNotificationOnInit" koanf:"disablenotificationoninit"`

// FileFormat (optional) is the format of the file to retrieve (available YAML, TOML and JSON)
// Default: YAML
FileFormat string `mapstructure:"fileFormat" koanf:"fileformat"`
Expand Down
1 change: 1 addition & 0 deletions cmd/relayproxy/service/gofeatureflag.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ func NewGoFeatureFlagClient(
DataExporter: exp,
StartWithRetrieverError: proxyConf.StartWithRetrieverError,
EnablePollingJitter: proxyConf.EnablePollingJitter,
DisableNotificationOnInit: proxyConf.DisableNotificationOnInit,
EvaluationContextEnrichment: proxyConf.EvaluationContextEnrichment,
PersistentFlagConfigurationFile: proxyConf.PersistentFlagConfigurationFile,
}
Expand Down
8 changes: 8 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ type Config struct {
// Default: false
EnablePollingJitter bool

// DisableNotificationOnInit (optional) set to true if you do not want to send
// a notification when the flags are loaded.
// This is useful if you do not want a Slack/Webhook notification saying that
// the flags have been added every time you start the application.
// Default is set to false for backward compatibility.
// Default: false
DisableNotificationOnInit bool

// Deprecated: Use LeveledLogger instead
// Logger (optional) logger use by the library
// Default: No log
Expand Down
45 changes: 39 additions & 6 deletions feature_flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func New(config Config) (*GoFeatureFlag, error) {
return nil, fmt.Errorf("impossible to initialize the retrievers, please check your configuration: %v", err)
}

err = retrieveFlagsAndUpdateCache(goFF.config, goFF.cache, goFF.retrieverManager)
err = retrieveFlagsAndInitializeCache(goFF.config, goFF.cache, goFF.retrieverManager)
if err != nil {
switch {
case config.PersistentFlagConfigurationFile != "":
Expand Down Expand Up @@ -154,7 +154,7 @@ func retrievePersistentLocalDisk(ctx context.Context, config Config, goFF *GoFea
return err
}
defer func() { _ = fallBackRetrieverManager.Shutdown(ctx) }()
err = retrieveFlagsAndUpdateCache(goFF.config, goFF.cache, fallBackRetrieverManager)
err = retrieveFlagsAndInitializeCache(goFF.config, goFF.cache, fallBackRetrieverManager)
if err != nil {
return err
}
Expand Down Expand Up @@ -203,8 +203,11 @@ func (g *GoFeatureFlag) startFlagUpdaterDaemon() {
}
}

// retrieveFlagsAndUpdateCache is called every X seconds to refresh the cache flag.
func retrieveFlagsAndUpdateCache(config Config, cache cache.Manager, retrieverManager *retriever.Manager) error {
func retreiveFlags(
config Config,
cache cache.Manager,
retrieverManager *retriever.Manager,
) (map[string]dto.DTO, error) {
retrievers := retrieverManager.GetRetrievers()
// Results is the type that will receive the results when calling
// all the retrievers.
Expand Down Expand Up @@ -250,7 +253,7 @@ func retrieveFlagsAndUpdateCache(config Config, cache cache.Manager, retrieverMa
retrieversResults := make([]map[string]dto.DTO, len(retrievers))
for v := range resultsChan {
if v.Error != nil {
return v.Error
return nil, v.Error
}
retrieversResults[v.Index] = v.Value
}
Expand All @@ -262,8 +265,38 @@ func retrieveFlagsAndUpdateCache(config Config, cache cache.Manager, retrieverMa
newFlags[flagName] = value
}
}
return newFlags, nil
}

// retrieveFlagsAndInitializeCache is called when the feature flag is initialized
func retrieveFlagsAndInitializeCache(config Config, cache cache.Manager, retrieverManager *retriever.Manager) error {
newFlags, err := retreiveFlags(config, cache, retrieverManager)
if err != nil {
return err
}

if config.DisableNotificationOnInit {
err = cache.UpdateCache(newFlags, config.internalLogger)
} else {
err = cache.UpdateCacheAndNotify(newFlags, config.internalLogger)
}

if err != nil {
log.Printf("error: impossible to initialize the cache of the flags: %v", err)
return err
}

return nil
}

// retrieveFlagsAndUpdateCache is called every X seconds to refresh the cache flag.
func retrieveFlagsAndUpdateCache(config Config, cache cache.Manager, retrieverManager *retriever.Manager) error {
newFlags, err := retreiveFlags(config, cache, retrieverManager)
if err != nil {
return err
}

err := cache.UpdateCache(newFlags, config.internalLogger)
err = cache.UpdateCacheAndNotify(newFlags, config.internalLogger)
if err != nil {
log.Printf("error: impossible to update the cache of the flags: %v", err)
return err
Expand Down
70 changes: 70 additions & 0 deletions feature_flag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"log"
"log/slog"
"os"
"sync"
"testing"
"time"

Expand All @@ -14,6 +15,7 @@ import (
"github.com/thomaspoignant/go-feature-flag/ffcontext"
"github.com/thomaspoignant/go-feature-flag/internal/flag"
"github.com/thomaspoignant/go-feature-flag/model"
"github.com/thomaspoignant/go-feature-flag/notifier"
"github.com/thomaspoignant/go-feature-flag/retriever"
"github.com/thomaspoignant/go-feature-flag/retriever/fileretriever"
"github.com/thomaspoignant/go-feature-flag/retriever/s3retriever"
Expand Down Expand Up @@ -706,3 +708,71 @@ func Test_UseCustomBucketingKey(t *testing.T) {
assert.Equal(t, want, got)
}
}

func Test_DisableNotificationOnInit(t *testing.T) {
tests := []struct {
name string
config *ffclient.Config
disableNotification bool
expectedNotifyCalled bool
}{
{
name: "DisableNotificationOnInit is true",
config: &ffclient.Config{
PollingInterval: 60 * time.Second,
Retriever: &fileretriever.Retriever{Path: "testdata/flag-config.yaml"},
DisableNotificationOnInit: true,
},
expectedNotifyCalled: false,
},
{
name: "DisableNotificationOnInit is false",
config: &ffclient.Config{
PollingInterval: 60 * time.Second,
Retriever: &fileretriever.Retriever{Path: "testdata/flag-config.yaml"},
DisableNotificationOnInit: false,
},
expectedNotifyCalled: true,
},
{
name: "DisableNotificationOnInit is not set",
config: &ffclient.Config{
PollingInterval: 60 * time.Second,
Retriever: &fileretriever.Retriever{Path: "testdata/flag-config.yaml"},
},
expectedNotifyCalled: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockNotifier := &mockNotificationService{}
tt.config.Notifiers = []notifier.Notifier{mockNotifier}

gffClient, err := ffclient.New(*tt.config)
assert.NoError(t, err)
defer gffClient.Close()

time.Sleep(2 * time.Second) // wait for the goroutine to call Notify()
assert.Equal(t, tt.expectedNotifyCalled, mockNotifier.notifyCalled)
})
}
}

type mockNotificationService struct {
notifyCalled bool
mu sync.Mutex
}

func (m *mockNotificationService) Notify(diff notifier.DiffCache) error {
m.mu.Lock()
defer m.mu.Unlock()
m.notifyCalled = true
return nil
}

func (m *mockNotificationService) wasNotifyCalled() bool {
m.mu.Lock()
defer m.mu.Unlock()
return m.notifyCalled
}
15 changes: 13 additions & 2 deletions internal/cache/cache_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
type Manager interface {
ConvertToFlagStruct(loadedFlags []byte, fileFormat string) (map[string]dto.DTO, error)
UpdateCache(newFlags map[string]dto.DTO, log *fflog.FFLogger) error
UpdateCacheAndNotify(newFlags map[string]dto.DTO, log *fflog.FFLogger) error
Close()
GetFlag(key string) (flag.Flag, error)
AllFlags() (map[string]flag.Flag, error)
Expand Down Expand Up @@ -61,6 +62,14 @@ func (c *cacheManagerImpl) ConvertToFlagStruct(loadedFlags []byte, fileFormat st
}

func (c *cacheManagerImpl) UpdateCache(newFlags map[string]dto.DTO, log *fflog.FFLogger) error {
return c.updateCache(newFlags, log, false)
}

func (c *cacheManagerImpl) UpdateCacheAndNotify(newFlags map[string]dto.DTO, log *fflog.FFLogger) error {
return c.updateCache(newFlags, log, true)
}

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

// notify the changes
c.notificationService.Notify(oldCacheFlags, newCacheFlags, log)
if notifyChanges {
// notify the changes
c.notificationService.Notify(oldCacheFlags, newCacheFlags, log)
}
// persist the cache on disk
if c.persistentFlagConfigurationFile != "" {
c.PersistCache(oldCacheFlags, newCacheFlags)
Expand Down
Loading