@@ -43,12 +43,13 @@ func Init(config Config) error {
43
43
// GoFeatureFlag is the main object of the library
44
44
// it contains the cache, the config, the updater and the exporter.
45
45
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
52
53
}
53
54
54
55
// ff is the default object for go-feature-flag
@@ -58,122 +59,156 @@ var onceFF sync.Once
58
59
// New creates a new go-feature-flag instances that retrieve the config from a YAML file
59
60
// and return everything you need to manage your flags.
60
61
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 )
72
63
if config .offlineMutex == nil {
73
64
config .offlineMutex = & sync.RWMutex {}
74
65
}
75
66
67
+ // initialize internal logger
76
68
config .internalLogger = & fflog.FFLogger {
77
69
LeveledLogger : config .LeveledLogger ,
78
70
LegacyLogger : config .Logger ,
79
71
}
80
72
81
73
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
83
82
}
84
83
85
- if ! config .Offline {
86
- notifiers := config .Notifiers
87
- notifiers = append (notifiers , & logsnotifier.Notifier {Logger : config .internalLogger })
84
+ notificationService := initializeNotificationService (config )
88
85
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 ,
94
98
)
99
+ }
100
+ goFF .retrieverManager = retrieverManager
95
101
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 {
98
105
return nil , err
99
106
}
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
+ }
112
108
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 ,
138
166
}
167
+ featureEventExporterConfigs = append (featureEventExporterConfigs , c )
139
168
}
169
+ }
140
170
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
+ }
148
179
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 )
167
194
}
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 ))
168
204
}
169
- config .internalLogger .Debug ("GO Feature Flag is initialized" )
170
- return goFF , nil
205
+ return nil
171
206
}
172
207
173
208
// retrievePersistentLocalDisk is a function used in case we are not able to retrieve any flag when starting
174
209
// GO Feature Flag.
175
210
// 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 {
177
212
if config .PersistentFlagConfigurationFile != "" {
178
213
config .internalLogger .Error (
179
214
"Impossible to retrieve your flag configuration, trying to use the persistent" +
@@ -183,7 +218,6 @@ func retrievePersistentLocalDisk(ctx context.Context, config Config, goFF *GoFea
183
218
if _ , err := os .Stat (config .PersistentFlagConfigurationFile ); err == nil {
184
219
// we found the configuration file on the disk
185
220
r := & fileretriever.Retriever {Path : config .PersistentFlagConfigurationFile }
186
-
187
221
fallBackRetrieverManager := retriever .NewManager (
188
222
config .Context ,
189
223
[]retriever.Retriever {r },
@@ -194,12 +228,7 @@ func retrievePersistentLocalDisk(ctx context.Context, config Config, goFF *GoFea
194
228
return err
195
229
}
196
230
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 )
203
232
if err != nil {
204
233
return err
205
234
}
@@ -215,16 +244,17 @@ func retrievePersistentLocalDisk(ctx context.Context, config Config, goFF *GoFea
215
244
func (g * GoFeatureFlag ) Close () {
216
245
if g != nil {
217
246
if g .cache != nil {
218
- // clear the cache
219
247
g .cache .Close ()
220
248
}
221
249
if g .bgUpdater .updaterChan != nil && g .bgUpdater .ticker != nil {
222
250
g .bgUpdater .close ()
223
251
}
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 ()
227
256
}
257
+
228
258
if g .retrieverManager != nil {
229
259
_ = g .retrieverManager .Shutdown (g .config .Context )
230
260
}
@@ -406,6 +436,5 @@ func ForceRefresh() bool {
406
436
// Close the component by stopping the background refresh and clean the cache.
407
437
func Close () {
408
438
onceFF = sync.Once {}
409
- ff .exporterWg .Wait ()
410
439
ff .Close ()
411
440
}
0 commit comments