github.com/SaurabhDubey-Groww/go-cloud@v0.0.0-20221124105541-b26c29285fd8/runtimevar/filevar/filevar.go (about)

     1  // Copyright 2018 The Go Cloud Development Kit Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package filevar provides a runtimevar implementation with variables
    16  // backed by the filesystem. Use OpenVariable to construct a *runtimevar.Variable.
    17  //
    18  // Configuration files can be updated using any commands (cp, mv) or
    19  // tools/editors. This package does not guarantee read consistency since
    20  // it does not have control over the writes. For example, some kinds of
    21  // updates might result in filevar temporarily receiving an error or an
    22  // empty value.
    23  //
    24  // Known Issues:
    25  //
    26  // * On macOS, if an empty file is copied into a configuration file,
    27  //
    28  //	filevar will not detect the change.
    29  //
    30  // # URLs
    31  //
    32  // For runtimevar.OpenVariable, filevar registers for the scheme "file".
    33  // To customize the URL opener, or for more details on the URL format,
    34  // see URLOpener.
    35  // See https://gocloud.dev/concepts/urls/ for background information.
    36  //
    37  // # As
    38  //
    39  // filevar does not support any types for As.
    40  package filevar // import "gocloud.dev/runtimevar/filevar"
    41  
    42  import (
    43  	"bytes"
    44  	"context"
    45  	"errors"
    46  	"fmt"
    47  	"io/ioutil"
    48  	"net/url"
    49  	"os"
    50  	"path/filepath"
    51  	"strings"
    52  	"time"
    53  
    54  	"github.com/fsnotify/fsnotify"
    55  	"gocloud.dev/gcerrors"
    56  	"gocloud.dev/runtimevar"
    57  	"gocloud.dev/runtimevar/driver"
    58  )
    59  
    60  func init() {
    61  	runtimevar.DefaultURLMux().RegisterVariable(Scheme, &URLOpener{})
    62  }
    63  
    64  // Scheme is the URL scheme filevar registers its URLOpener under on runtimevar.DefaultMux.
    65  const Scheme = "file"
    66  
    67  // URLOpener opens filevar URLs like "file:///path/to/config.json?decoder=json".
    68  //
    69  // The URL's host+path is used as the path to the file to watch.
    70  // If os.PathSeparator != "/", any leading "/" from the path is dropped
    71  // and remaining '/' characters are converted to os.PathSeparator.
    72  //
    73  // The following URL parameters are supported:
    74  //   - decoder: The decoder to use. Defaults to URLOpener.Decoder, or
    75  //     runtimevar.BytesDecoder if URLOpener.Decoder is nil.
    76  //     See runtimevar.DecoderByName for supported values.
    77  //   - wait: The frequency for retries after an error, in time.ParseDuration formats.
    78  //     Defaults to 30s.
    79  type URLOpener struct {
    80  	// Decoder specifies the decoder to use if one is not specified in the URL.
    81  	// Defaults to runtimevar.BytesDecoder.
    82  	Decoder *runtimevar.Decoder
    83  
    84  	// Options specifies the options to pass to OpenVariable.
    85  	Options Options
    86  }
    87  
    88  // OpenVariableURL opens the variable at the URL's path. See the package doc
    89  // for more details.
    90  func (o *URLOpener) OpenVariableURL(ctx context.Context, u *url.URL) (*runtimevar.Variable, error) {
    91  	q := u.Query()
    92  
    93  	decoderName := q.Get("decoder")
    94  	q.Del("decoder")
    95  	decoder, err := runtimevar.DecoderByName(ctx, decoderName, o.Decoder)
    96  	if err != nil {
    97  		return nil, fmt.Errorf("open variable %v: invalid decoder: %v", u, err)
    98  	}
    99  	opts := o.Options
   100  	if s := q.Get("wait"); s != "" {
   101  		q.Del("wait")
   102  		d, err := time.ParseDuration(s)
   103  		if err != nil {
   104  			return nil, fmt.Errorf("open variable %v: invalid wait %q: %v", u, s, err)
   105  		}
   106  		opts.WaitDuration = d
   107  	}
   108  
   109  	for param := range q {
   110  		return nil, fmt.Errorf("open variable %v: invalid query parameter %q", u, param)
   111  	}
   112  	path := u.Path
   113  	if os.PathSeparator != '/' {
   114  		path = strings.TrimPrefix(path, "/")
   115  	}
   116  	return OpenVariable(filepath.FromSlash(path), decoder, &opts)
   117  }
   118  
   119  // Options sets options.
   120  type Options struct {
   121  	// WaitDuration controls the frequency of retries after an error. For example,
   122  	// if the file does not exist. Defaults to 30 seconds.
   123  	WaitDuration time.Duration
   124  }
   125  
   126  // OpenVariable constructs a *runtimevar.Variable backed by the file at path.
   127  // The file holds raw bytes; provide a decoder to decode the raw bytes into the
   128  // appropriate type for runtimevar.Snapshot.Value.
   129  // See the runtimevar package documentation for examples of decoders.
   130  func OpenVariable(path string, decoder *runtimevar.Decoder, opts *Options) (*runtimevar.Variable, error) {
   131  	w, err := newWatcher(path, decoder, opts)
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  	return runtimevar.New(w), nil
   136  }
   137  
   138  func newWatcher(path string, decoder *runtimevar.Decoder, opts *Options) (*watcher, error) {
   139  	if opts == nil {
   140  		opts = &Options{}
   141  	}
   142  	if path == "" {
   143  		return nil, errors.New("path is required")
   144  	}
   145  	if decoder == nil {
   146  		return nil, errors.New("decoder is required")
   147  	}
   148  
   149  	// Use absolute file path.
   150  	abspath, err := filepath.Abs(path)
   151  	if err != nil {
   152  		return nil, err
   153  	}
   154  
   155  	// Construct a fsnotify.Watcher.
   156  	notifier, err := fsnotify.NewWatcher()
   157  	if err != nil {
   158  		return nil, err
   159  	}
   160  
   161  	// Create a ctx for the background goroutine that does all of the reading.
   162  	// The cancel function will be used to shut it down during Close, with the
   163  	// result being passed back via closeCh.
   164  	ctx, cancel := context.WithCancel(context.Background())
   165  	w := &watcher{
   166  		path: abspath,
   167  		// See struct comments for why it's buffered.
   168  		ch:       make(chan *state, 1),
   169  		closeCh:  make(chan error),
   170  		shutdown: cancel,
   171  	}
   172  	go w.watch(ctx, notifier, abspath, decoder, driver.WaitDuration(opts.WaitDuration))
   173  	return w, nil
   174  }
   175  
   176  // errNotExist wraps an underlying error in cases where the file likely doesn't
   177  // exist.
   178  type errNotExist struct {
   179  	err error
   180  }
   181  
   182  func (e *errNotExist) Error() string {
   183  	return e.err.Error()
   184  }
   185  
   186  // state implements driver.State.
   187  type state struct {
   188  	val        interface{}
   189  	updateTime time.Time
   190  	raw        []byte
   191  	err        error
   192  }
   193  
   194  func (s *state) Value() (interface{}, error) {
   195  	return s.val, s.err
   196  }
   197  
   198  func (s *state) UpdateTime() time.Time {
   199  	return s.updateTime
   200  }
   201  
   202  func (s *state) As(i interface{}) bool {
   203  	return false
   204  }
   205  
   206  // watcher implements driver.Watcher for configurations stored in files.
   207  type watcher struct {
   208  	// The path for the file we're watching.
   209  	path string
   210  	// The background goroutine writes new *state values to ch.
   211  	// It is buffered so that the background goroutine can write without
   212  	// blocking; it always drains the buffer before writing so that the latest
   213  	// write is buffered. If writes could block, the background goroutine could be
   214  	// blocked indefinitely from reading fsnotify events.
   215  	ch chan *state
   216  	// closeCh is used to return any errors from closing the notifier
   217  	// back to watcher.Close.
   218  	closeCh chan error
   219  	// shutdown tells the background goroutine to exit.
   220  	shutdown func()
   221  }
   222  
   223  // WatchVariable implements driver.WatchVariable.
   224  func (w *watcher) WatchVariable(ctx context.Context, _ driver.State) (driver.State, time.Duration) {
   225  	select {
   226  	case <-ctx.Done():
   227  		return &state{err: ctx.Err()}, 0
   228  	case cur := <-w.ch:
   229  		return cur, 0
   230  	}
   231  }
   232  
   233  // updateState checks to see if s and prev both represent the same error.
   234  // If not, it drains any previous state buffered in w.ch, then writes s to it.
   235  // It always return s.
   236  func (w *watcher) updateState(s, prev *state) *state {
   237  	if s.err != nil && prev != nil && prev.err != nil && (s.err == prev.err || s.err.Error() == prev.err.Error() || (os.IsNotExist(s.err) && os.IsNotExist(prev.err))) {
   238  		// s represents the same error as prev.
   239  		return s
   240  	}
   241  	// Drain any buffered value on ch; it is now stale.
   242  	select {
   243  	case <-w.ch:
   244  	default:
   245  	}
   246  	// This write can't block, since we're the only writer, ch has a buffer
   247  	// size of 1, and we just read anything that was buffered.
   248  	w.ch <- s
   249  	return s
   250  }
   251  
   252  // watch is run by a background goroutine.
   253  // It watches file using notifier, and writes new states to w.ch.
   254  // If it can't read or watch the file, it re-checks every wait.
   255  // It exits when ctx is canceled, and writes any shutdown errors (or
   256  // nil if there weren't any) to w.closeCh.
   257  func (w *watcher) watch(ctx context.Context, notifier *fsnotify.Watcher, file string, decoder *runtimevar.Decoder, wait time.Duration) {
   258  	var cur *state
   259  
   260  	for {
   261  		// If the current state is an error, pause between attempts
   262  		// to avoid spin loops. In particular, this happens when the file
   263  		// doesn't exist.
   264  		if cur != nil && cur.err != nil {
   265  			select {
   266  			case <-ctx.Done():
   267  				w.closeCh <- notifier.Close()
   268  				return
   269  			case <-time.After(wait):
   270  			}
   271  		}
   272  
   273  		// Add the file to the notifier to be watched. It's fine to be
   274  		// added multiple times, and fsnotifier is a bit flaky about when
   275  		// it's needed during renames, so just always try.
   276  		if err := notifier.Add(file); err != nil {
   277  			// File probably does not exist. Try again later.
   278  			cur = w.updateState(&state{err: &errNotExist{err}}, cur)
   279  			continue
   280  		}
   281  
   282  		// Read the file.
   283  		b, err := ioutil.ReadFile(file)
   284  		if err != nil {
   285  			// File probably does not exist. Try again later.
   286  			cur = w.updateState(&state{err: &errNotExist{err}}, cur)
   287  			continue
   288  		}
   289  
   290  		// If it's a new value, decode and return it.
   291  		if cur == nil || cur.err != nil || !bytes.Equal(cur.raw, b) {
   292  			if val, err := decoder.Decode(ctx, b); err != nil {
   293  				cur = w.updateState(&state{err: err}, cur)
   294  			} else {
   295  				cur = w.updateState(&state{val: val, updateTime: time.Now(), raw: b}, cur)
   296  			}
   297  		}
   298  
   299  		// Block until notifier tells us something relevant changed.
   300  		wait := true
   301  		for wait {
   302  			select {
   303  			case <-ctx.Done():
   304  				w.closeCh <- notifier.Close()
   305  				return
   306  
   307  			case event := <-notifier.Events:
   308  				if event.Name != file {
   309  					continue
   310  				}
   311  				// Ignore if not one of the following operations.
   312  				if event.Op&(fsnotify.Create|fsnotify.Write|fsnotify.Remove|fsnotify.Rename) == 0 {
   313  					continue
   314  				}
   315  				wait = false
   316  
   317  			case err := <-notifier.Errors:
   318  				cur = w.updateState(&state{err: err}, cur)
   319  			}
   320  		}
   321  	}
   322  }
   323  
   324  // Close implements driver.WatchVariable.
   325  func (w *watcher) Close() error {
   326  	// Tell the background goroutine to shut down by canceling its ctx.
   327  	w.shutdown()
   328  	// Wait for it to return the result of closing the notifier.
   329  	err := <-w.closeCh
   330  	// Cleanup our channels.
   331  	close(w.ch)
   332  	close(w.closeCh)
   333  	return err
   334  }
   335  
   336  // ErrorAs implements driver.ErrorAs.
   337  func (w *watcher) ErrorAs(err error, i interface{}) bool { return false }
   338  
   339  // ErrorCode implements driver.ErrorCode.
   340  func (*watcher) ErrorCode(err error) gcerrors.ErrorCode {
   341  	if _, ok := err.(*errNotExist); ok {
   342  		return gcerrors.NotFound
   343  	}
   344  	return gcerrors.Unknown
   345  }