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