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  }