code.vegaprotocol.io/vega@v0.79.0/datanode/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 "sync" 22 "sync/atomic" 23 "time" 24 25 "code.vegaprotocol.io/vega/logging" 26 "code.vegaprotocol.io/vega/paths" 27 28 "github.com/fsnotify/fsnotify" 29 ) 30 31 const ( 32 namedLogger = "cfgwatcher" 33 ) 34 35 // Watcher is looking for updates in the configurations files. 36 type Watcher struct { 37 log *logging.Logger 38 cfg Config 39 configFilePath string 40 41 // to be used as an atomic 42 hasChanged atomic.Bool 43 cfgUpdateListeners []func(Config) 44 cfgHandlers []func(*Config) error 45 mu sync.Mutex 46 } 47 48 type Option func(w *Watcher) 49 50 func Use(use func(*Config) error) Option { 51 fn := func(w *Watcher) { 52 w.Use(use) 53 } 54 55 return fn 56 } 57 58 // NewWatcher instantiate a new watcher from the vega config files. 59 func NewWatcher(ctx context.Context, log *logging.Logger, vegaPaths paths.Paths, opts ...Option) (*Watcher, error) { 60 watcherLog := log.Named(namedLogger) 61 // set this logger to debug level as we want to be notified for any configuration changes at any time 62 watcherLog.SetLevel(logging.DebugLevel) 63 64 configFilePath, err := vegaPaths.CreateConfigPathFor(paths.DataNodeDefaultConfigFile) 65 if err != nil { 66 return nil, fmt.Errorf("couldn't get path for %s: %w", paths.NodeDefaultConfigFile, err) 67 } 68 69 w := &Watcher{ 70 log: watcherLog, 71 cfg: NewDefaultConfig(), 72 configFilePath: configFilePath, 73 cfgUpdateListeners: []func(Config){}, 74 } 75 76 for _, opt := range opts { 77 opt(w) 78 } 79 80 err = w.load() 81 if err != nil { 82 return nil, err 83 } 84 85 watcher, err := fsnotify.NewWatcher() 86 if err != nil { 87 return nil, err 88 } 89 err = watcher.Add(w.configFilePath) 90 if err != nil { 91 return nil, err 92 } 93 94 w.log.Info("config watcher started successfully", 95 logging.String("config", w.configFilePath)) 96 97 go w.watch(ctx, watcher) 98 99 return w, nil 100 } 101 102 func (w *Watcher) OnTimeUpdate(_ context.Context, _ time.Time) { 103 if !w.hasChanged.Load() { 104 // no changes we can return straight away 105 return 106 } 107 // get the config and updates listeners 108 cfg := w.Get() 109 110 for _, f := range w.cfgUpdateListeners { 111 f(cfg) 112 } 113 114 // reset the atomic 115 w.hasChanged.Store(false) 116 } 117 118 // Get return the last update of the configuration. 119 func (w *Watcher) Get() Config { 120 w.mu.Lock() 121 conf := w.cfg 122 w.mu.Unlock() 123 return conf 124 } 125 126 // OnConfigUpdate register a function to be called when the configuration is getting updated. 127 func (w *Watcher) OnConfigUpdate(fns ...func(Config)) { 128 w.mu.Lock() 129 w.cfgUpdateListeners = append(w.cfgUpdateListeners, fns...) 130 w.mu.Unlock() 131 } 132 133 // Use registers a function that modify the config when the configuration is updated. 134 func (w *Watcher) Use(fns ...func(*Config) error) { 135 w.mu.Lock() 136 w.cfgHandlers = append(w.cfgHandlers, fns...) 137 w.mu.Unlock() 138 } 139 140 func (w *Watcher) load() error { 141 w.mu.Lock() 142 defer w.mu.Unlock() 143 144 if err := paths.ReadStructuredFile(w.configFilePath, &w.cfg); err != nil { 145 return fmt.Errorf("couldn't read configuration file at %s: %w", w.configFilePath, err) 146 } 147 148 for _, f := range w.cfgHandlers { 149 if err := f(&w.cfg); err != nil { 150 return err 151 } 152 } 153 154 return nil 155 } 156 157 func (w *Watcher) watch(ctx context.Context, watcher *fsnotify.Watcher) { 158 defer watcher.Close() 159 for { 160 select { 161 case event := <-watcher.Events: 162 if event.Has(fsnotify.Write) || event.Has(fsnotify.Rename) { 163 if event.Has(fsnotify.Rename) { 164 // add a small sleep here in order to handle vi as 165 // vi does not send a write event / edit the file in place, 166 // it always creates a temporary file, then deletes the original one, 167 // and then renames the temp file with the name of the original file. 168 // if we try to update the conf as soon as we get the event, the file is not 169 // always created and we get a no such file or directory error 170 time.Sleep(50 * time.Millisecond) 171 } 172 w.log.Info("configuration updated", logging.String("event", event.Name)) 173 err := w.load() 174 if err != nil { 175 w.log.Error("unable to load configuration", logging.Error(err)) 176 continue 177 } 178 w.hasChanged.Store(true) 179 } 180 case err := <-watcher.Errors: 181 w.log.Error("config watcher received error event", logging.Error(err)) 182 case <-ctx.Done(): 183 w.log.Error("config watcher ctx done") 184 return 185 } 186 } 187 }