diff --git a/background_updater.go b/background_updater.go new file mode 100644 index 00000000000..754aff017c6 --- /dev/null +++ b/background_updater.go @@ -0,0 +1,24 @@ +package ffclient + +import "time" + +// backgroundUpdater contains what is needed to manage the +// background update of the flags. +type backgroundUpdater struct { + ticker *time.Ticker + updaterChan chan struct{} +} + +// newBackgroundUpdater init default value for the ticker and the channel. +func newBackgroundUpdater(pollInterval int) backgroundUpdater { + return backgroundUpdater{ + ticker: time.NewTicker(time.Duration(pollInterval) * time.Second), + updaterChan: make(chan struct{}), + } +} + +// close stop the ticker and close the channel. +func (bgu *backgroundUpdater) close() { + bgu.ticker.Stop() + close(bgu.updaterChan) +} diff --git a/feature_flag.go b/feature_flag.go index d97a65a2fc0..ac7eeece1e0 100644 --- a/feature_flag.go +++ b/feature_flag.go @@ -2,7 +2,6 @@ package ffclient import ( "fmt" - "github.com/go-co-op/gocron" "log" "sync" "time" @@ -35,9 +34,9 @@ func Close() { // GoFeatureFlag is the main object of the library // it contains the cache, the config and the update. type GoFeatureFlag struct { - flagUpdater gocron.Scheduler - cache cache.Cache - config Config + cache cache.Cache + config Config + bgUpdater backgroundUpdater } // ff is the default object for go-feature-flag @@ -47,54 +46,55 @@ var onceFF sync.Once // New creates a new go-feature-flag instance that retrieve the config from a YAML file // and return everything you need to manage your flags. func New(config Config) (*GoFeatureFlag, error) { - flagUpdater := *gocron.NewScheduler(time.UTC) - // The default value for poll interval is 60 seconds if config.PollInterval == 0 { config.PollInterval = 60 } + // Check that value is not negative + if config.PollInterval < 0 { + return nil, fmt.Errorf("%d is not a valid PollInterval value, it need to be > 0", config.PollInterval) + } + goFF := &GoFeatureFlag{ - cache: cache.New(config.Logger), - flagUpdater: flagUpdater, - config: config, + cache: cache.New(config.Logger), + config: config, + bgUpdater: newBackgroundUpdater(config.PollInterval), } - err := goFF.startUpdater() + // fail if we cannot retrieve the flags the 1st time + err := retrieveFlagsAndUpdateCache(goFF.config, goFF.cache) if err != nil { - return nil, err + return nil, fmt.Errorf("impossible to retrieve the flags, please check your configuration: %v", err) } + // start the flag update in background + go goFF.startFlagUpdaterDaemon() + return goFF, nil } func (g *GoFeatureFlag) Close() { + // clear the cache g.cache.Close() - g.flagUpdater.Stop() -} -func (g *GoFeatureFlag) startUpdater() error { - // fail if we cannot retrieve the flags the 1st time - err := retrieveFlagsAndUpdateCache(g.config, g.cache) - if err != nil { - return fmt.Errorf("impossible to retrieve the flags, please check your configuration: %v", err) - } - - if g.config.PollInterval < 0 { - return fmt.Errorf("%d is not a valid PollInterval value, it need to be > 0", g.config.PollInterval) - } - - // start flag updater - _, err = g.flagUpdater. - Every(uint64(g.config.PollInterval)). - Seconds(). - Do(retrieveFlagsAndUpdateCache, g.config, g.cache) + // stop the background updater + g.bgUpdater.close() +} - if err != nil { - return fmt.Errorf("impossible to launch background updater: %v", err) +// startFlagUpdaterDaemon is the daemon that refresh the cache every X seconds. +func (g *GoFeatureFlag) startFlagUpdaterDaemon() { + for { + select { + case <-g.bgUpdater.ticker.C: + err := retrieveFlagsAndUpdateCache(g.config, g.cache) + if err != nil && g.config.Logger != nil { + g.config.Logger.Printf("[%v] error while updating the cache: %v\n", time.Now().Format(time.RFC3339), err) + } + case <-g.bgUpdater.updaterChan: + return + } } - g.flagUpdater.StartAsync() - return nil } // retrieveFlagsAndUpdateCache is called every X seconds to refresh the cache flag. diff --git a/feature_flag_test.go b/feature_flag_test.go index 72348d4f952..96eb8aff79f 100644 --- a/feature_flag_test.go +++ b/feature_flag_test.go @@ -3,9 +3,11 @@ package ffclient import ( "github.com/aws/aws-sdk-go/aws" "github.com/stretchr/testify/assert" + "io/ioutil" "log" "os" "testing" + "time" "github.com/thomaspoignant/go-feature-flag/ffuser" ) @@ -85,3 +87,41 @@ func Test2GoFeatureFlagInstance(t *testing.T) { hasTestFlagClient2, _ := gffClient2.BoolVariation("test-flag", user, false) assert.False(t, hasTestFlagClient2, "User should have test flag") } + +func TestUpdateFlag(t *testing.T) { + initialFileContent := `test-flag: + rule: key eq "random-key" + percentage: 100 + true: true + false: false + default: false` + + flagFile, _ := ioutil.TempFile("", "") + _ = ioutil.WriteFile(flagFile.Name(), []byte(initialFileContent), 0600) + + gffClient1, _ := New(Config{ + PollInterval: 1, + Retriever: &FileRetriever{Path: flagFile.Name()}, + }) + defer gffClient1.Close() + + flagValue, _ := gffClient1.BoolVariation("test-flag", ffuser.NewUser("random-key"), false) + assert.True(t, flagValue) + + updatedFileContent := `test-flag: + rule: key eq "random-key2" + percentage: 100 + true: true + false: false + default: false` + + _ = ioutil.WriteFile(flagFile.Name(), []byte(updatedFileContent), 0600) + + flagValue, _ = gffClient1.BoolVariation("test-flag", ffuser.NewUser("random-key"), false) + assert.True(t, flagValue) + + time.Sleep(2 * time.Second) + + flagValue, _ = gffClient1.BoolVariation("test-flag", ffuser.NewUser("random-key"), false) + assert.False(t, flagValue) +} diff --git a/go.mod b/go.mod index 85a293972d4..e7d93c93453 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ require ( github.com/antlr/antlr4 v0.0.0-20201206235148-c87e55b61113 // indirect github.com/aws/aws-sdk-go v1.36.19 github.com/blang/semver v3.5.1+incompatible // indirect - github.com/go-co-op/gocron v0.4.0 github.com/google/go-cmp v0.5.4 github.com/nikunjy/rules v0.0.0-20200120082459-0b7c4dc9dc86 github.com/stretchr/testify v1.6.1 diff --git a/go.sum b/go.sum index 8ec314862d5..8f804145563 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,6 @@ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdn github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-co-op/gocron v0.4.0 h1:MO9iUktaVn03seJUDGEelPGL3SME9P+Ot9VdTAdorQw= -github.com/go-co-op/gocron v0.4.0/go.mod h1:6Btk4lVj3bnFAgbVfr76W8impTyhYrEi1pV5Pt4Tp/M= github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= @@ -20,8 +18,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -40,8 +36,6 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IV golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 2e54e9b72b2..7b80933e75b 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -20,14 +20,14 @@ type Cache interface { type cacheImpl struct { Logger *log.Logger flagsCache map[string]flags.Flag - mutex sync.Mutex + mutex sync.RWMutex waitGroup sync.WaitGroup } func New(logger *log.Logger) Cache { return &cacheImpl{ flagsCache: make(map[string]flags.Flag), - mutex: sync.Mutex{}, + mutex: sync.RWMutex{}, Logger: logger, waitGroup: sync.WaitGroup{}, } @@ -55,8 +55,13 @@ func (c *cacheImpl) UpdateCache(loadedFlags []byte) error { } func (c *cacheImpl) Close() { + // Wait for the logs to finish c.waitGroup.Wait() + + // Clear the cache + c.mutex.Lock() c.flagsCache = nil + c.mutex.Unlock() } func (c *cacheImpl) getCacheCopy() map[string]flags.Flag { @@ -68,6 +73,9 @@ func (c *cacheImpl) getCacheCopy() map[string]flags.Flag { } func (c *cacheImpl) GetFlag(key string) (flags.Flag, error) { + c.mutex.RLock() + defer c.mutex.RUnlock() + if c.flagsCache == nil { return flags.Flag{}, errors.New("impossible to read the toggle before the initialisation") } @@ -103,7 +111,7 @@ func (c *cacheImpl) logFlagChanges(oldCache map[string]flags.Flag, newCache map[ } } else if !cmp.Equal(oldCache[key], newCache[key]) { // key has changed in cache - c.Logger.Printf("[%v] flag %s updated, old=[%v], new=[%v]\n", date, key, c.flagsCache[key], newCache[key]) + c.Logger.Printf("[%v] flag %s updated, old=[%v], new=[%v]\n", date, key, oldCache[key], newCache[key]) } } diff --git a/internal/cache/cache_test.go b/internal/cache/cache_test.go index e302a67cf49..59ae5f63210 100644 --- a/internal/cache/cache_test.go +++ b/internal/cache/cache_test.go @@ -200,7 +200,7 @@ add-test-flag: fCache := cacheImpl{ flagsCache: oldValue, - mutex: sync.Mutex{}, + mutex: sync.RWMutex{}, Logger: log.New(logOutput, "", 0), waitGroup: sync.WaitGroup{}, } diff --git a/variation_test.go b/variation_test.go index 767db896c9c..9f7021469e1 100644 --- a/variation_test.go +++ b/variation_test.go @@ -2,13 +2,11 @@ package ffclient import ( "errors" - "github.com/go-co-op/gocron" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "io/ioutil" "log" "testing" - "time" "github.com/thomaspoignant/go-feature-flag/ffuser" "github.com/thomaspoignant/go-feature-flag/internal/cache" @@ -168,8 +166,8 @@ func TestBoolVariation(t *testing.T) { logger := log.New(file, "", 0) ff = &GoFeatureFlag{ - flagUpdater: *gocron.NewScheduler(time.UTC), - cache: tt.args.cacheMock, + bgUpdater: newBackgroundUpdater(5), + cache: tt.args.cacheMock, config: Config{ PollInterval: 0, Logger: logger, @@ -330,8 +328,8 @@ func TestFloat64Variation(t *testing.T) { logger := log.New(file, "", 0) ff = &GoFeatureFlag{ - flagUpdater: *gocron.NewScheduler(time.UTC), - cache: tt.args.cacheMock, + bgUpdater: newBackgroundUpdater(5), + cache: tt.args.cacheMock, config: Config{ PollInterval: 0, Logger: logger, @@ -492,8 +490,8 @@ func TestJSONArrayVariation(t *testing.T) { logger := log.New(file, "", 0) ff = &GoFeatureFlag{ - flagUpdater: *gocron.NewScheduler(time.UTC), - cache: tt.args.cacheMock, + bgUpdater: newBackgroundUpdater(5), + cache: tt.args.cacheMock, config: Config{ PollInterval: 0, Logger: logger, @@ -654,8 +652,8 @@ func TestJSONVariation(t *testing.T) { logger := log.New(file, "", 0) ff = &GoFeatureFlag{ - flagUpdater: *gocron.NewScheduler(time.UTC), - cache: tt.args.cacheMock, + bgUpdater: newBackgroundUpdater(5), + cache: tt.args.cacheMock, config: Config{ PollInterval: 0, Logger: logger, @@ -818,8 +816,8 @@ func TestStringVariation(t *testing.T) { logger := log.New(file, "", 0) ff = &GoFeatureFlag{ - flagUpdater: *gocron.NewScheduler(time.UTC), - cache: tt.args.cacheMock, + bgUpdater: newBackgroundUpdater(5), + cache: tt.args.cacheMock, config: Config{ PollInterval: 0, Logger: logger, @@ -980,8 +978,8 @@ func TestIntVariation(t *testing.T) { logger := log.New(file, "", 0) ff = &GoFeatureFlag{ - flagUpdater: *gocron.NewScheduler(time.UTC), - cache: tt.args.cacheMock, + bgUpdater: newBackgroundUpdater(5), + cache: tt.args.cacheMock, config: Config{ PollInterval: 0, Logger: logger,