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  }