-
-
Notifications
You must be signed in to change notification settings - Fork 163
/
Copy pathcache_manager.go
165 lines (150 loc) · 4.46 KB
/
cache_manager.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
package cache
import (
"encoding/json"
"errors"
"log/slog"
"os"
"strings"
"sync"
"time"
"github.com/BurntSushi/toml"
"github.com/google/go-cmp/cmp"
"github.com/thomaspoignant/go-feature-flag/internal/flag"
"github.com/thomaspoignant/go-feature-flag/internal/notification"
"github.com/thomaspoignant/go-feature-flag/model/dto"
"github.com/thomaspoignant/go-feature-flag/utils/fflog"
"gopkg.in/yaml.v3"
)
type Manager interface {
ConvertToFlagStruct(loadedFlags []byte, fileFormat string) (map[string]dto.DTO, error)
UpdateCache(newFlags map[string]dto.DTO, log *fflog.FFLogger, notifyChanges bool) error
Close()
GetFlag(key string) (flag.Flag, error)
AllFlags() (map[string]flag.Flag, error)
GetLatestUpdateDate() time.Time
}
type cacheManagerImpl struct {
inMemoryCache Cache
mutex sync.RWMutex
notificationService notification.Service
latestUpdate time.Time
logger *fflog.FFLogger
persistentFlagConfigurationFile string
}
func New(
notificationService notification.Service,
persistentFlagConfigurationFile string,
logger *fflog.FFLogger,
) Manager {
return &cacheManagerImpl{
logger: logger,
inMemoryCache: NewInMemoryCache(logger),
mutex: sync.RWMutex{},
notificationService: notificationService,
persistentFlagConfigurationFile: persistentFlagConfigurationFile,
}
}
func (c *cacheManagerImpl) ConvertToFlagStruct(
loadedFlags []byte,
fileFormat string,
) (map[string]dto.DTO, error) {
var newFlags map[string]dto.DTO
var err error
switch strings.ToLower(fileFormat) {
case "toml":
err = toml.Unmarshal(loadedFlags, &newFlags)
case "json":
err = json.Unmarshal(loadedFlags, &newFlags)
default:
// default unmarshaller is YAML
err = yaml.Unmarshal(loadedFlags, &newFlags)
}
return newFlags, err
}
func (c *cacheManagerImpl) UpdateCache(
newFlags map[string]dto.DTO,
log *fflog.FFLogger,
notifyChanges bool,
) error {
newCache := NewInMemoryCache(c.logger)
newCache.Init(newFlags)
newCacheFlags := newCache.All()
oldCacheFlags := map[string]flag.Flag{}
c.mutex.Lock()
// collect flags for compare.
if c.inMemoryCache != nil {
oldCacheFlags = c.inMemoryCache.All()
}
c.inMemoryCache = newCache
c.latestUpdate = time.Now()
c.mutex.Unlock()
if notifyChanges {
// notify the changes
c.notificationService.Notify(oldCacheFlags, newCacheFlags, log)
}
// persist the cache on disk
if c.persistentFlagConfigurationFile != "" {
c.PersistCache(oldCacheFlags, newCacheFlags)
}
return nil
}
func (c *cacheManagerImpl) Close() {
// Clear the cache
c.mutex.Lock()
c.inMemoryCache = nil
c.mutex.Unlock()
if c.notificationService != nil {
c.notificationService.Close()
}
}
func (c *cacheManagerImpl) GetFlag(key string) (flag.Flag, error) {
c.mutex.RLock()
defer c.mutex.RUnlock()
if c.inMemoryCache == nil {
return nil, errors.New("impossible to read the flag before the initialisation")
}
return c.inMemoryCache.getFlag(key)
}
func (c *cacheManagerImpl) AllFlags() (map[string]flag.Flag, error) {
c.mutex.RLock()
defer c.mutex.RUnlock()
if c.inMemoryCache == nil {
return nil, errors.New("impossible to read the flag before the initialisation")
}
return c.inMemoryCache.All(), nil
}
func (c *cacheManagerImpl) GetLatestUpdateDate() time.Time {
c.mutex.RLock()
defer c.mutex.RUnlock()
return c.latestUpdate
}
// PersistCache is writing the flags to a file to be able to restart without being able to access the retrievers.
// It is useful to have a fallback in case of a problem with the retrievers, such as a network issue.
//
// The persistence is done in a goroutine to not block the main thread.
func (c *cacheManagerImpl) PersistCache(
oldCache map[string]flag.Flag,
newCache map[string]flag.Flag,
) {
go func() {
if _, err := os.Stat(c.persistentFlagConfigurationFile); !os.IsNotExist(err) &&
cmp.Equal(oldCache, newCache) {
c.logger.Debug("No change in the cache, skipping the persist")
return
}
data, err := yaml.Marshal(newCache)
if err != nil {
c.logger.Error("Error while marshalling flags to persist", slog.Any("error", err))
return
}
err = os.WriteFile(c.persistentFlagConfigurationFile, data, 0600)
if err != nil {
c.logger.Error("Error while writing flags to file", slog.Any("error", err))
return
}
c.logger.Info(
"Flags cache persisted to file",
slog.String("file", c.persistentFlagConfigurationFile),
)
}()
}