forked from thomaspoignant/go-feature-flag
-
Notifications
You must be signed in to change notification settings - Fork 0
/
feature_flag.go
354 lines (313 loc) · 11.2 KB
/
feature_flag.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
package ffclient
import (
"context"
"fmt"
"log"
"log/slog"
"os"
"sync"
"time"
"github.com/thomaspoignant/go-feature-flag/exporter"
"github.com/thomaspoignant/go-feature-flag/internal/cache"
"github.com/thomaspoignant/go-feature-flag/model/dto"
"github.com/thomaspoignant/go-feature-flag/notifier/logsnotifier"
"github.com/thomaspoignant/go-feature-flag/retriever"
"github.com/thomaspoignant/go-feature-flag/retriever/fileretriever"
"github.com/thomaspoignant/go-feature-flag/utils/fflog"
)
// Init the feature flag component with the configuration of ffclient.Config
//
// func main() {
// err := ffclient.Init(ffclient.Config{
// PollingInterval: 3 * time.Second,
// Retriever: &httpretriever.Retriever{
// URL: "http://example.com/flag-config.yaml",
// },
// })
// defer ffclient.Close()
func Init(config Config) error {
var err error
onceFF.Do(func() {
var tmpFF *GoFeatureFlag
tmpFF, err = New(config)
if err == nil {
ff = tmpFF
}
})
return err
}
// GoFeatureFlag is the main object of the library
// it contains the cache, the config, the updater and the exporter.
type GoFeatureFlag struct {
cache cache.Manager
config Config
bgUpdater backgroundUpdater
dataExporter *exporter.Scheduler
retrieverManager *retriever.Manager
}
// ff is the default object for go-feature-flag
var ff *GoFeatureFlag
var onceFF sync.Once
// New creates a new go-feature-flag instances that retrieve the config from a YAML file
// and return everything you need to manage your flags.
func New(config Config) (*GoFeatureFlag, error) {
switch {
case config.PollingInterval == 0:
// The default value for the poll interval is 60 seconds
config.PollingInterval = 60 * time.Second
case config.PollingInterval < 0:
// Check that value is not negative
return nil, fmt.Errorf("%d is not a valid PollingInterval value, it need to be > 0", config.PollingInterval)
case config.PollingInterval < time.Second:
// the minimum value for the polling policy is 1 second
config.PollingInterval = time.Second
default:
// do nothing
}
if config.offlineMutex == nil {
config.offlineMutex = &sync.RWMutex{}
}
config.internalLogger = &fflog.FFLogger{
LeveledLogger: config.LeveledLogger,
LegacyLogger: config.Logger,
}
goFF := &GoFeatureFlag{
config: config,
}
if !config.Offline {
notifiers := config.Notifiers
notifiers = append(notifiers, &logsnotifier.Notifier{Logger: config.internalLogger})
notificationService := cache.NewNotificationService(notifiers)
goFF.bgUpdater = newBackgroundUpdater(config.PollingInterval, config.EnablePollingJitter)
goFF.cache = cache.New(notificationService, config.PersistentFlagConfigurationFile, config.internalLogger)
retrievers, err := config.GetRetrievers()
if err != nil {
return nil, err
}
goFF.retrieverManager = retriever.NewManager(config.Context, retrievers, config.internalLogger)
err = goFF.retrieverManager.Init(config.Context)
if err != nil && !config.StartWithRetrieverError {
return nil, fmt.Errorf("impossible to initialize the retrievers, please check your configuration: %v", err)
}
err = retrieveFlagsAndUpdateCache(goFF.config, goFF.cache, goFF.retrieverManager, true)
if err != nil {
switch {
case config.PersistentFlagConfigurationFile != "":
errPersist := retrievePersistentLocalDisk(config.Context, config, goFF)
if errPersist != nil && !config.StartWithRetrieverError {
return nil, fmt.Errorf("impossible to use the persistent flag configuration file: %v "+
"[original error: %v]", errPersist, err)
}
case !config.StartWithRetrieverError:
return nil, fmt.Errorf("impossible to retrieve the flags, please check your configuration: %v", err)
default:
// We accept to start with a retriever error, we will serve only default value
goFF.config.internalLogger.Error("Impossible to retrieve the flags, starting with the "+
"retriever error", slog.Any("error", err))
}
}
go goFF.startFlagUpdaterDaemon()
if goFF.config.DataExporter.Exporter != nil {
// init the data exporter
goFF.dataExporter = exporter.NewScheduler(goFF.config.Context, goFF.config.DataExporter.FlushInterval,
goFF.config.DataExporter.MaxEventInMemory, goFF.config.DataExporter.Exporter, goFF.config.internalLogger)
// we start the daemon only if we have a bulk exporter
if goFF.config.DataExporter.Exporter.IsBulk() {
go goFF.dataExporter.StartDaemon()
}
}
}
config.internalLogger.Debug("GO Feature Flag is initialized")
return goFF, nil
}
// retrievePersistentLocalDisk is a function used in case we are not able to retrieve any flag when starting
// GO Feature Flag.
// This function will look at any pre-existent persistent configuration and start with it.
func retrievePersistentLocalDisk(ctx context.Context, config Config, goFF *GoFeatureFlag) error {
if config.PersistentFlagConfigurationFile != "" {
config.internalLogger.Error("Impossible to retrieve your flag configuration, trying to use the persistent"+
" flag configuration file.", slog.String("path", config.PersistentFlagConfigurationFile))
if _, err := os.Stat(config.PersistentFlagConfigurationFile); err == nil {
// we found the configuration file on the disk
r := &fileretriever.Retriever{Path: config.PersistentFlagConfigurationFile}
fallBackRetrieverManager := retriever.NewManager(config.Context, []retriever.Retriever{r}, config.internalLogger)
err := fallBackRetrieverManager.Init(ctx)
if err != nil {
return err
}
defer func() { _ = fallBackRetrieverManager.Shutdown(ctx) }()
err = retrieveFlagsAndUpdateCache(goFF.config, goFF.cache, fallBackRetrieverManager, true)
if err != nil {
return err
}
return nil
}
config.internalLogger.Warn("No persistent flag configuration found",
slog.String("path", config.PersistentFlagConfigurationFile))
}
return fmt.Errorf("no persistent flag available")
}
// Close wait until thread are done
func (g *GoFeatureFlag) Close() {
if g != nil {
if g.cache != nil {
// clear the cache
g.cache.Close()
}
if g.bgUpdater.updaterChan != nil && g.bgUpdater.ticker != nil {
g.bgUpdater.close()
}
if g.dataExporter != nil {
g.dataExporter.Close()
}
if g.retrieverManager != nil {
_ = g.retrieverManager.Shutdown(g.config.Context)
}
}
}
// startFlagUpdaterDaemon is the daemon that refreshes the cache every X seconds.
func (g *GoFeatureFlag) startFlagUpdaterDaemon() {
for {
select {
case <-g.bgUpdater.ticker.C:
if !g.IsOffline() {
err := retrieveFlagsAndUpdateCache(g.config, g.cache, g.retrieverManager, false)
if err != nil {
g.config.internalLogger.Error("Error while updating the cache.", slog.Any("error", err))
}
}
case <-g.bgUpdater.updaterChan:
return
}
}
}
// retreiveFlags is a function that will retrieve the flags from the retrievers,
// merge them and convert them to the flag struct.
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.
type Results struct {
Error error
Value map[string]dto.DTO
Index int
}
// resultsChan is the channel that will receive all the results.
resultsChan := make(chan Results)
var wg sync.WaitGroup
wg.Add(len(retrievers))
// Launching a goroutine that will wait until the waiting group is complete.
// It closes the channel when ready
go func() {
wg.Wait()
close(resultsChan)
}()
for index, r := range retrievers {
// Launching GO routines to retrieve all files in parallel.
go func(r retriever.Retriever, format string, index int, ctx context.Context) {
defer wg.Done()
// If the retriever is not ready, we ignore it
if rr, ok := r.(retriever.CommonInitializableRetriever); ok && rr.Status() != retriever.RetrieverReady {
resultsChan <- Results{Error: nil, Value: map[string]dto.DTO{}, Index: index}
return
}
rawValue, err := r.Retrieve(ctx)
if err != nil {
resultsChan <- Results{Error: err, Value: nil, Index: index}
return
}
convertedFlag, err := cache.ConvertToFlagStruct(rawValue, format)
resultsChan <- Results{Error: err, Value: convertedFlag, Index: index}
}(r, config.FileFormat, index, config.Context)
}
retrieversResults := make([]map[string]dto.DTO, len(retrievers))
for v := range resultsChan {
if v.Error != nil {
return nil, v.Error
}
retrieversResults[v.Index] = v.Value
}
// merge all the flags
newFlags := map[string]dto.DTO{}
for _, flags := range retrieversResults {
for flagName, value := range flags {
newFlags[flagName] = value
}
}
return newFlags, nil
}
// retrieveFlagsAndUpdateCache is a function that retrieves the flags from the retrievers,
// and update the cache with the new flags.
func retrieveFlagsAndUpdateCache(config Config, cache cache.Manager,
retrieverManager *retriever.Manager, isInit bool) error {
newFlags, err := retreiveFlags(config, cache, retrieverManager)
if err != nil {
return err
}
err = cache.UpdateCache(newFlags, config.internalLogger, !(isInit && config.DisableNotifierOnInit))
if err != nil {
log.Printf("error: impossible to update the cache of the flags: %v", err)
return err
}
return nil
}
// GetCacheRefreshDate gives the last refresh date of the cache
func (g *GoFeatureFlag) GetCacheRefreshDate() time.Time {
if g.config.Offline {
return time.Time{}
}
return g.cache.GetLatestUpdateDate()
}
// ForceRefresh is a function that forces to call the retrievers and refresh the configuration of flags.
// This function can be called explicitly to refresh the flags if you know that a change has been made in
// the configuration.
func (g *GoFeatureFlag) ForceRefresh() bool {
if g.IsOffline() {
return false
}
err := retrieveFlagsAndUpdateCache(g.config, g.cache, g.retrieverManager, false)
if err != nil {
g.config.internalLogger.Error("Error while force updating the cache.", slog.Any("error", err))
return false
}
return true
}
// SetOffline updates the config Offline parameter
func (g *GoFeatureFlag) SetOffline(control bool) {
g.config.SetOffline(control)
}
// IsOffline allows knowing if the feature flag is in offline mode
func (g *GoFeatureFlag) IsOffline() bool {
return g.config.IsOffline()
}
// GetPollingInterval is the polling interval between two refreshes of the cache
func (g *GoFeatureFlag) GetPollingInterval() int64 {
return g.config.PollingInterval.Milliseconds()
}
// SetOffline updates the config Offline parameter
func SetOffline(control bool) {
ff.SetOffline(control)
}
// IsOffline allows knowing if the feature flag is in offline mode
func IsOffline() bool {
return ff.IsOffline()
}
// GetCacheRefreshDate gives the last refresh date of the cache
func GetCacheRefreshDate() time.Time {
return ff.GetCacheRefreshDate()
}
// ForceRefresh is a function that forces to call the retrievers and refresh the configuration of flags.
// This function can be called explicitly to refresh the flags if you know that a change has been made in
// the configuration.
func ForceRefresh() bool {
return ff.ForceRefresh()
}
// Close the component by stopping the background refresh and clean the cache.
func Close() {
onceFF = sync.Once{}
ff.Close()
}