code.vegaprotocol.io/vega@v0.79.0/core/config/watcher.go (about) 1 // Copyright (C) 2023 Gobalsky Labs Limited 2 // 3 // This program is free software: you can redistribute it and/or modify 4 // it under the terms of the GNU Affero General Public License as 5 // published by the Free Software Foundation, either version 3 of the 6 // License, or (at your option) any later version. 7 // 8 // This program is distributed in the hope that it will be useful, 9 // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 // GNU Affero General Public License for more details. 12 // 13 // You should have received a copy of the GNU Affero General Public License 14 // along with this program. If not, see <http://www.gnu.org/licenses/>. 15 16 package config 17 18 import ( 19 "context" 20 "fmt" 21 "sort" 22 "sync" 23 "sync/atomic" 24 "time" 25 26 "code.vegaprotocol.io/vega/logging" 27 "code.vegaprotocol.io/vega/paths" 28 29 "github.com/fsnotify/fsnotify" 30 ) 31 32 const ( 33 namedLogger = "cfgwatcher" 34 ) 35 36 // Watcher is looking for updates in the configurations files. 37 type Watcher struct { 38 log *logging.Logger 39 cfg Config 40 configFilePath string 41 42 // to be used as an atomic 43 hasChanged atomic.Bool 44 cfgUpdateListeners []func(Config) 45 cfgHandlers []func(*Config) error 46 47 // listeners with IDs 48 cfgUpdateListenersWithID map[int]func(Config) 49 currentID int 50 mu sync.Mutex 51 } 52 53 type Option func(w *Watcher) 54 55 func Use(use func(*Config) error) Option { 56 fn := func(w *Watcher) { 57 w.Use(use) 58 } 59 60 return fn 61 } 62 63 // NewWatcher instantiate a new watcher from the vega config files. 64 func NewWatcher(ctx context.Context, log *logging.Logger, vegaPaths paths.Paths, migrateConfig func(*Config), opts ...Option) (*Watcher, error) { 65 watcherLog := log.Named(namedLogger) 66 // set this logger to debug level as we want to be notified for any configuration changes at any time 67 watcherLog.SetLevel(logging.DebugLevel) 68 69 configFilePath, err := vegaPaths.CreateConfigPathFor(paths.NodeDefaultConfigFile) 70 if err != nil { 71 return nil, fmt.Errorf("couldn't get path for %s: %w", paths.NodeDefaultConfigFile, err) 72 } 73 74 w := &Watcher{ 75 log: watcherLog, 76 cfg: NewDefaultConfig(), 77 configFilePath: configFilePath, 78 cfgUpdateListeners: []func(Config){}, 79 cfgUpdateListenersWithID: map[int]func(Config){}, 80 } 81 82 for _, opt := range opts { 83 opt(w) 84 } 85 86 err = w.load(migrateConfig) 87 if err != nil { 88 return nil, err 89 } 90 91 watcher, err := fsnotify.NewWatcher() 92 if err != nil { 93 return nil, err 94 } 95 err = watcher.Add(w.configFilePath) 96 if err != nil { 97 return nil, err 98 } 99 100 go w.watch(ctx, watcher) 101 102 return w, nil 103 } 104 105 func (w *Watcher) OnTimeUpdate(_ context.Context, _ time.Time) { 106 if !w.hasChanged.Load() { 107 // no changes we can return straight away 108 return 109 } 110 // get the config and updates listeners 111 cfg := w.Get() 112 113 for _, f := range w.cfgUpdateListeners { 114 f(cfg) 115 } 116 117 ids := []int{} 118 for k := range w.cfgUpdateListenersWithID { 119 ids = append(ids, k) 120 } 121 sort.Ints(ids) 122 123 for id := range ids { 124 w.cfgUpdateListenersWithID[id](cfg) 125 } 126 127 // reset the atomic 128 w.hasChanged.Store(false) 129 } 130 131 // Get return the last update of the configuration. 132 func (w *Watcher) Get() Config { 133 w.mu.Lock() 134 conf := w.cfg 135 w.mu.Unlock() 136 return conf 137 } 138 139 // OnConfigUpdate register a function to be called when the configuration is getting updated. 140 func (w *Watcher) OnConfigUpdate(fns ...func(Config)) { 141 w.mu.Lock() 142 w.cfgUpdateListeners = append(w.cfgUpdateListeners, fns...) 143 w.mu.Unlock() 144 } 145 146 // OnConfigUpdateWithID register a function to be called when the configuration 147 // is getting updated. 148 func (w *Watcher) OnConfigUpdateWithID(fns ...func(Config)) []int { 149 w.mu.Lock() 150 // w.cfgUpdateListeners = append(w.cfgUpdateListeners, fns...) 151 ids := []int{} 152 for _, f := range fns { 153 id := w.currentID 154 ids = append(ids, id) 155 w.cfgUpdateListenersWithID[id] = f 156 w.currentID++ 157 } 158 w.mu.Unlock() 159 return ids 160 } 161 162 func (w *Watcher) Unregister(ids []int) { 163 for _, id := range ids { 164 delete(w.cfgUpdateListenersWithID, id) 165 } 166 } 167 168 // Use registers a function that modify the config when the configuration is updated. 169 func (w *Watcher) Use(fns ...func(*Config) error) { 170 w.mu.Lock() 171 w.cfgHandlers = append(w.cfgHandlers, fns...) 172 w.mu.Unlock() 173 } 174 175 func (w *Watcher) load(migrateConfig func(*Config)) error { 176 w.mu.Lock() 177 defer w.mu.Unlock() 178 179 if err := paths.ReadStructuredFile(w.configFilePath, &w.cfg); err != nil { 180 return fmt.Errorf("couldn't read configuration file at %s: %w", w.configFilePath, err) 181 } 182 183 if migrateConfig != nil { 184 migrateConfig(&w.cfg) 185 if err := paths.WriteStructuredFile(w.configFilePath, &w.cfg); err != nil { 186 return fmt.Errorf("couldn't write migrated configuration file at %s: %w", w.configFilePath, err) 187 } 188 } 189 190 for _, f := range w.cfgHandlers { 191 if err := f(&w.cfg); err != nil { 192 return err 193 } 194 } 195 196 return nil 197 } 198 199 func (w *Watcher) watch(ctx context.Context, watcher *fsnotify.Watcher) { 200 defer watcher.Close() 201 for { 202 select { 203 case event := <-watcher.Events: 204 if event.Has(fsnotify.Write) || event.Has(fsnotify.Rename) { 205 if event.Has(fsnotify.Rename) { 206 // add a small sleep here in order to handle vi 207 // vi do not send a write event / edit the file in place, 208 // it always create a temporary file, then delete the original one, 209 // and then rename the temp file with the name of the original file. 210 // if we try to update the conf as soon as we get the event, the file is not 211 // always created and we get a no such file or directory error 212 time.Sleep(50 * time.Millisecond) 213 } 214 w.log.Info("configuration updated", logging.String("event", event.Name)) 215 err := w.load(nil) 216 if err != nil { 217 w.log.Error("unable to load configuration", logging.Error(err)) 218 continue 219 } 220 w.hasChanged.Store(true) 221 } 222 case err := <-watcher.Errors: 223 w.log.Error("config watcher received error event", logging.Error(err)) 224 case <-ctx.Done(): 225 w.log.Error("config watcher ctx done") 226 return 227 } 228 } 229 }