Skip to content

Commit 62a480b

Browse files
fix: Refactor ffclient.New to be readable (#3276)
Signed-off-by: Thomas Poignant <[email protected]>
1 parent 4416662 commit 62a480b

File tree

3 files changed

+151
-122
lines changed

3 files changed

+151
-122
lines changed

Diff for: feature_flag.go

+134-105
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,13 @@ func Init(config Config) error {
4343
// GoFeatureFlag is the main object of the library
4444
// it contains the cache, the config, the updater and the exporter.
4545
type GoFeatureFlag struct {
46-
cache cache.Manager
47-
config Config
48-
bgUpdater backgroundUpdater
49-
dataExporter exporter.Manager[exporter.FeatureEvent]
50-
retrieverManager *retriever.Manager
51-
exporterWg sync.WaitGroup
46+
cache cache.Manager
47+
config Config
48+
bgUpdater backgroundUpdater
49+
featureEventDataExporter exporter.Manager[exporter.FeatureEvent]
50+
retrieverManager *retriever.Manager
51+
// evalExporterWg is a wait group to wait for the evaluation exporter to finish the export before closing GOFF
52+
evalExporterWg sync.WaitGroup
5253
}
5354

5455
// ff is the default object for go-feature-flag
@@ -58,122 +59,156 @@ var onceFF sync.Once
5859
// New creates a new go-feature-flag instances that retrieve the config from a YAML file
5960
// and return everything you need to manage your flags.
6061
func New(config Config) (*GoFeatureFlag, error) {
61-
switch {
62-
case config.PollingInterval == 0:
63-
// The default value for the poll interval is 60 seconds
64-
config.PollingInterval = 60 * time.Second
65-
case config.PollingInterval > 0 && config.PollingInterval < time.Second:
66-
// the minimum value for the polling policy is 1 second
67-
config.PollingInterval = time.Second
68-
default:
69-
// do nothing
70-
}
71-
62+
config.PollingInterval = adjustPollingInterval(config.PollingInterval)
7263
if config.offlineMutex == nil {
7364
config.offlineMutex = &sync.RWMutex{}
7465
}
7566

67+
// initialize internal logger
7668
config.internalLogger = &fflog.FFLogger{
7769
LeveledLogger: config.LeveledLogger,
7870
LegacyLogger: config.Logger,
7971
}
8072

8173
goFF := &GoFeatureFlag{
82-
config: config,
74+
config: config,
75+
evalExporterWg: sync.WaitGroup{},
76+
}
77+
78+
if config.Offline {
79+
// in case we are in offline mode, we don't need to initialize the cache since we will not use it.
80+
goFF.config.internalLogger.Info("GO Feature Flag is in offline mode")
81+
return goFF, nil
8382
}
8483

85-
if !config.Offline {
86-
notifiers := config.Notifiers
87-
notifiers = append(notifiers, &logsnotifier.Notifier{Logger: config.internalLogger})
84+
notificationService := initializeNotificationService(config)
8885

89-
notificationService := cache.NewNotificationService(notifiers)
90-
goFF.cache = cache.New(
91-
notificationService,
92-
config.PersistentFlagConfigurationFile,
93-
config.internalLogger,
86+
// init internal cache
87+
goFF.cache = cache.New(
88+
notificationService,
89+
config.PersistentFlagConfigurationFile,
90+
config.internalLogger,
91+
)
92+
93+
retrieverManager, err := initializeRetrieverManager(config)
94+
if err != nil && (retrieverManager == nil || !config.StartWithRetrieverError) {
95+
return nil, fmt.Errorf(
96+
"impossible to initialize the retrievers, please check your configuration: %v",
97+
err,
9498
)
99+
}
100+
goFF.retrieverManager = retrieverManager
95101

96-
retrievers, err := config.GetRetrievers()
97-
if err != nil {
102+
// first retrieval of the flags
103+
if err := retrieveFlagsAndUpdateCache(goFF.config, goFF.cache, goFF.retrieverManager, true); err != nil {
104+
if err := handleFirstRetrieverError(config, goFF.config.internalLogger, goFF.cache, err); err != nil {
98105
return nil, err
99106
}
100-
goFF.retrieverManager = retriever.NewManager(
101-
config.Context,
102-
retrievers,
103-
config.internalLogger,
104-
)
105-
err = goFF.retrieverManager.Init(config.Context)
106-
if err != nil && !config.StartWithRetrieverError {
107-
return nil, fmt.Errorf(
108-
"impossible to initialize the retrievers, please check your configuration: %v",
109-
err,
110-
)
111-
}
107+
}
112108

113-
err = retrieveFlagsAndUpdateCache(goFF.config, goFF.cache, goFF.retrieverManager, true)
114-
if err != nil {
115-
switch {
116-
case config.PersistentFlagConfigurationFile != "":
117-
errPersist := retrievePersistentLocalDisk(config.Context, config, goFF)
118-
if errPersist != nil && !config.StartWithRetrieverError {
119-
return nil, fmt.Errorf(
120-
"impossible to use the persistent flag configuration file: %v "+
121-
"[original error: %v]",
122-
errPersist,
123-
err,
124-
)
125-
}
126-
case !config.StartWithRetrieverError:
127-
return nil, fmt.Errorf(
128-
"impossible to retrieve the flags, please check your configuration: %v",
129-
err,
130-
)
131-
default:
132-
// We accept to start with a retriever error, we will serve only default value
133-
goFF.config.internalLogger.Error(
134-
"Impossible to retrieve the flags, starting with the "+
135-
"retriever error",
136-
slog.Any("error", err),
137-
)
109+
// start the background task to update the flags periodically
110+
if config.PollingInterval > 0 {
111+
goFF.bgUpdater = newBackgroundUpdater(config.PollingInterval, config.EnablePollingJitter)
112+
go goFF.startFlagUpdaterDaemon()
113+
}
114+
115+
goFF.featureEventDataExporter =
116+
initializeDataExporters(config, goFF.config.internalLogger)
117+
config.internalLogger.Debug("GO Feature Flag is initialized")
118+
return goFF, nil
119+
}
120+
121+
// adjustPollingInterval is a function that will check the polling interval and set it to the minimum value if it is
122+
// lower than 1 second. It also set the default value to 60 seconds if the polling interval is 0.
123+
func adjustPollingInterval(pollingInterval time.Duration) time.Duration {
124+
switch {
125+
case pollingInterval == 0:
126+
// The default value for the poll interval is 60 seconds
127+
return 60 * time.Second
128+
case pollingInterval > 0 && pollingInterval < time.Second:
129+
// the minimum value for the polling policy is 1 second
130+
return time.Second
131+
default:
132+
return pollingInterval
133+
}
134+
}
135+
136+
// initializeNotificationService is a function that will initialize the notification service with the notifiers
137+
func initializeNotificationService(config Config) cache.Service {
138+
notifiers := config.Notifiers
139+
notifiers = append(notifiers, &logsnotifier.Notifier{Logger: config.internalLogger})
140+
return cache.NewNotificationService(notifiers)
141+
}
142+
143+
// initializeRetrieverManager is a function that will initialize the retriever manager with the retrievers
144+
func initializeRetrieverManager(config Config) (*retriever.Manager, error) {
145+
retrievers, err := config.GetRetrievers()
146+
if err != nil {
147+
return nil, err
148+
}
149+
manager := retriever.NewManager(config.Context, retrievers, config.internalLogger)
150+
err = manager.Init(config.Context)
151+
return manager, err
152+
}
153+
154+
func initializeDataExporters(
155+
config Config,
156+
logger *fflog.FFLogger,
157+
) exporter.Manager[exporter.FeatureEvent] {
158+
exporters := config.GetDataExporters()
159+
featureEventExporterConfigs := make([]exporter.Config, 0)
160+
if len(exporters) > 0 {
161+
for _, exp := range exporters {
162+
c := exporter.Config{
163+
Exporter: exp.Exporter,
164+
FlushInterval: exp.FlushInterval,
165+
MaxEventInMemory: exp.MaxEventInMemory,
138166
}
167+
featureEventExporterConfigs = append(featureEventExporterConfigs, c)
139168
}
169+
}
140170

141-
if config.PollingInterval > 0 {
142-
goFF.bgUpdater = newBackgroundUpdater(
143-
config.PollingInterval,
144-
config.EnablePollingJitter,
145-
)
146-
go goFF.startFlagUpdaterDaemon()
147-
}
171+
var featureEventManager exporter.Manager[exporter.FeatureEvent]
172+
if len(featureEventExporterConfigs) > 0 {
173+
featureEventManager = exporter.NewManager[exporter.FeatureEvent](
174+
config.Context, featureEventExporterConfigs, config.ExporterCleanQueueInterval, logger)
175+
featureEventManager.Start()
176+
}
177+
return featureEventManager
178+
}
148179

149-
exporters := goFF.config.GetDataExporters()
150-
if len(exporters) > 0 {
151-
// init the data exporter
152-
expConfigs := make([]exporter.Config, len(exporters))
153-
for index, exp := range exporters {
154-
expConfigs[index] = exporter.Config{
155-
Exporter: exp.Exporter,
156-
FlushInterval: exp.FlushInterval,
157-
MaxEventInMemory: exp.MaxEventInMemory,
158-
}
159-
}
160-
goFF.dataExporter = exporter.NewManager[exporter.FeatureEvent](
161-
config.Context,
162-
expConfigs,
163-
config.ExporterCleanQueueInterval,
164-
goFF.config.internalLogger,
165-
)
166-
go goFF.dataExporter.Start()
180+
// handleFirstRetrieverError is a function that will handle the first error when trying to retrieve
181+
// the flags the first time when starting GO Feature Flag.
182+
func handleFirstRetrieverError(
183+
config Config,
184+
logger *fflog.FFLogger,
185+
cache cache.Manager,
186+
err error,
187+
) error {
188+
switch {
189+
case config.PersistentFlagConfigurationFile != "":
190+
errPersist := retrievePersistentLocalDisk(config.Context, config, cache)
191+
if errPersist != nil && !config.StartWithRetrieverError {
192+
return fmt.Errorf("impossible to use the persistent flag configuration file: %v "+
193+
"[original error: %v]", errPersist, err)
167194
}
195+
case !config.StartWithRetrieverError:
196+
return fmt.Errorf(
197+
"impossible to retrieve the flags, please check your configuration: %v",
198+
err,
199+
)
200+
default:
201+
// We accept to start with a retriever error, we will serve only default value
202+
logger.Error("Impossible to retrieve the flags, starting with the "+
203+
"retriever error", slog.Any("error", err))
168204
}
169-
config.internalLogger.Debug("GO Feature Flag is initialized")
170-
return goFF, nil
205+
return nil
171206
}
172207

173208
// retrievePersistentLocalDisk is a function used in case we are not able to retrieve any flag when starting
174209
// GO Feature Flag.
175210
// This function will look at any pre-existent persistent configuration and start with it.
176-
func retrievePersistentLocalDisk(ctx context.Context, config Config, goFF *GoFeatureFlag) error {
211+
func retrievePersistentLocalDisk(ctx context.Context, config Config, cache cache.Manager) error {
177212
if config.PersistentFlagConfigurationFile != "" {
178213
config.internalLogger.Error(
179214
"Impossible to retrieve your flag configuration, trying to use the persistent"+
@@ -183,7 +218,6 @@ func retrievePersistentLocalDisk(ctx context.Context, config Config, goFF *GoFea
183218
if _, err := os.Stat(config.PersistentFlagConfigurationFile); err == nil {
184219
// we found the configuration file on the disk
185220
r := &fileretriever.Retriever{Path: config.PersistentFlagConfigurationFile}
186-
187221
fallBackRetrieverManager := retriever.NewManager(
188222
config.Context,
189223
[]retriever.Retriever{r},
@@ -194,12 +228,7 @@ func retrievePersistentLocalDisk(ctx context.Context, config Config, goFF *GoFea
194228
return err
195229
}
196230
defer func() { _ = fallBackRetrieverManager.Shutdown(ctx) }()
197-
err = retrieveFlagsAndUpdateCache(
198-
goFF.config,
199-
goFF.cache,
200-
fallBackRetrieverManager,
201-
true,
202-
)
231+
err = retrieveFlagsAndUpdateCache(config, cache, fallBackRetrieverManager, true)
203232
if err != nil {
204233
return err
205234
}
@@ -215,16 +244,17 @@ func retrievePersistentLocalDisk(ctx context.Context, config Config, goFF *GoFea
215244
func (g *GoFeatureFlag) Close() {
216245
if g != nil {
217246
if g.cache != nil {
218-
// clear the cache
219247
g.cache.Close()
220248
}
221249
if g.bgUpdater.updaterChan != nil && g.bgUpdater.ticker != nil {
222250
g.bgUpdater.close()
223251
}
224-
225-
if g.dataExporter != nil {
226-
g.dataExporter.Stop()
252+
// we have to wait for the GO routine before stopping the exporter
253+
g.evalExporterWg.Wait()
254+
if g.featureEventDataExporter != nil {
255+
g.featureEventDataExporter.Stop()
227256
}
257+
228258
if g.retrieverManager != nil {
229259
_ = g.retrieverManager.Shutdown(g.config.Context)
230260
}
@@ -406,6 +436,5 @@ func ForceRefresh() bool {
406436
// Close the component by stopping the background refresh and clean the cache.
407437
func Close() {
408438
onceFF = sync.Once{}
409-
ff.exporterWg.Wait()
410439
ff.Close()
411440
}

Diff for: variation.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -303,9 +303,9 @@ func (g *GoFeatureFlag) getFlagFromCache(flagKey string) (flag.Flag, error) {
303303

304304
// CollectEventData is collecting events and sending them to the data exporter to be stored.
305305
func (g *GoFeatureFlag) CollectEventData(event exporter.FeatureEvent) {
306-
if g != nil && g.dataExporter != nil {
306+
if g != nil && g.featureEventDataExporter != nil {
307307
// Add event in the exporter
308-
g.dataExporter.AddEvent(event)
308+
g.featureEventDataExporter.AddEvent(event)
309309
}
310310
}
311311

@@ -328,9 +328,9 @@ func notifyVariation[T model.JSONType](
328328
"SERVER",
329329
ctx.ExtractGOFFProtectedFields().ExporterMetadata,
330330
)
331-
g.exporterWg.Add(1)
331+
g.evalExporterWg.Add(1)
332332
go func() {
333-
defer g.exporterWg.Done()
333+
defer g.evalExporterWg.Done()
334334
g.CollectEventData(event)
335335
}()
336336
}

0 commit comments

Comments
 (0)