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

Remove gocron dependency #59

Merged
merged 4 commits into from
Jan 19, 2021
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
24 changes: 24 additions & 0 deletions background_updater.go
Original file line number Diff line number Diff line change
@@ -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)
}
66 changes: 33 additions & 33 deletions feature_flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package ffclient

import (
"fmt"
"github.com/go-co-op/gocron"
"log"
"sync"
"time"
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
40 changes: 40 additions & 0 deletions feature_flag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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)
}
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 0 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
Expand All @@ -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=
Expand Down
14 changes: 11 additions & 3 deletions internal/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{},
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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")
}
Expand Down Expand Up @@ -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])
}
}

Expand Down
2 changes: 1 addition & 1 deletion internal/cache/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{},
}
Expand Down
26 changes: 12 additions & 14 deletions variation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down