vitess.io/vitess@v0.16.2/go/vt/topo/consultopo/watch.go (about)

     1  /*
     2  Copyright 2019 The Vitess Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package consultopo
    18  
    19  import (
    20  	"context"
    21  	"path"
    22  	"time"
    23  
    24  	"github.com/hashicorp/consul/api"
    25  	"github.com/spf13/pflag"
    26  
    27  	"vitess.io/vitess/go/vt/servenv"
    28  	"vitess.io/vitess/go/vt/topo"
    29  )
    30  
    31  var (
    32  	watchPollDuration = 30 * time.Second
    33  )
    34  
    35  func init() {
    36  	servenv.RegisterFlagsForTopoBinaries(registerWatchFlags)
    37  }
    38  
    39  func registerWatchFlags(fs *pflag.FlagSet) {
    40  	fs.DurationVar(&watchPollDuration, "topo_consul_watch_poll_duration", watchPollDuration, "time of the long poll for watch queries.")
    41  }
    42  
    43  // Watch is part of the topo.Conn interface.
    44  func (s *Server) Watch(ctx context.Context, filePath string) (*topo.WatchData, <-chan *topo.WatchData, error) {
    45  	// Initial get.
    46  	nodePath := path.Join(s.root, filePath)
    47  	options := &api.QueryOptions{}
    48  
    49  	initialCtx, initialCancel := context.WithTimeout(ctx, topo.RemoteOperationTimeout)
    50  	defer initialCancel()
    51  
    52  	pair, _, err := s.kv.Get(nodePath, options.WithContext(initialCtx))
    53  	if err != nil {
    54  		return nil, nil, err
    55  	}
    56  	if pair == nil {
    57  		// Node doesn't exist.
    58  		return nil, nil, topo.NewError(topo.NoNode, nodePath)
    59  	}
    60  
    61  	// Initial value to return.
    62  	wd := &topo.WatchData{
    63  		Contents: pair.Value,
    64  		Version:  ConsulVersion(pair.ModifyIndex),
    65  	}
    66  
    67  	// Create the notifications channel, send updates to it.
    68  	notifications := make(chan *topo.WatchData, 10)
    69  	go func() {
    70  		defer close(notifications)
    71  
    72  		var getCtx context.Context
    73  		// Initialize to no-op function to avoid having to check for nil.
    74  		cancelGetCtx := func() {}
    75  
    76  		defer cancelGetCtx()
    77  
    78  		for {
    79  			// Wait/poll until we get a new version.
    80  			// Get with a WaitIndex and WaitTime will return
    81  			// the current version at the end of WaitTime
    82  			// if it didn't change. So we just check for that
    83  			// and swallow the notifications when version matches.
    84  			waitIndex := pair.ModifyIndex
    85  			opts := &api.QueryOptions{
    86  				WaitIndex: waitIndex,
    87  				WaitTime:  watchPollDuration,
    88  			}
    89  
    90  			// Make a new Context for just this one Get() call.
    91  			// The server should send us something after WaitTime at the latest.
    92  			// If it takes more than 2x that long, assume we've lost contact.
    93  			// This essentially uses WaitTime as a heartbeat interval to detect
    94  			// a dead connection.
    95  			cancelGetCtx()
    96  			getCtx, cancelGetCtx = context.WithTimeout(ctx, 2*opts.WaitTime)
    97  
    98  			pair, _, err = s.kv.Get(nodePath, opts.WithContext(getCtx))
    99  			if err != nil {
   100  				// Serious error or context timeout/cancelled.
   101  				notifications <- &topo.WatchData{
   102  					Err: convertError(err, nodePath),
   103  				}
   104  				cancelGetCtx()
   105  				return
   106  			}
   107  
   108  			// If the node disappeared, pair is nil.
   109  			if pair == nil {
   110  				notifications <- &topo.WatchData{
   111  					Err: topo.NewError(topo.NoNode, nodePath),
   112  				}
   113  				cancelGetCtx()
   114  				return
   115  			}
   116  
   117  			// If we got a new value, send it.
   118  			if pair.ModifyIndex != waitIndex {
   119  				notifications <- &topo.WatchData{
   120  					Contents: pair.Value,
   121  					Version:  ConsulVersion(pair.ModifyIndex),
   122  				}
   123  			}
   124  
   125  			// See if the watch was canceled.
   126  			select {
   127  			case <-ctx.Done():
   128  				notifications <- &topo.WatchData{
   129  					Err: convertError(ctx.Err(), nodePath),
   130  				}
   131  				cancelGetCtx()
   132  				return
   133  			default:
   134  			}
   135  		}
   136  	}()
   137  
   138  	return wd, notifications, nil
   139  }
   140  
   141  // WatchRecursive is part of the topo.Conn interface.
   142  func (s *Server) WatchRecursive(_ context.Context, path string) ([]*topo.WatchDataRecursive, <-chan *topo.WatchDataRecursive, error) {
   143  	// This isn't implemented yet, but likely can be implemented using List
   144  	// with blocking logic like how we use Get with blocking for regular Watch.
   145  	// See also how https://www.consul.io/docs/dynamic-app-config/watches#keyprefix
   146  	// works under the hood.
   147  	return nil, nil, topo.NewError(topo.NoImplementation, path)
   148  }