github.com/xmidt-org/webpa-common@v1.11.9/service/consul/instancer.go (about)

     1  package consul
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"reflect"
     7  	"sort"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/go-kit/kit/log"
    12  	"github.com/go-kit/kit/log/level"
    13  	"github.com/go-kit/kit/sd"
    14  	"github.com/go-kit/kit/util/conn"
    15  	"github.com/hashicorp/consul/api"
    16  	"github.com/xmidt-org/webpa-common/logging"
    17  )
    18  
    19  var (
    20  	errStopped = errors.New("Instancer stopped")
    21  )
    22  
    23  type InstancerOptions struct {
    24  	Client       Client
    25  	Logger       log.Logger
    26  	Service      string
    27  	Tags         []string
    28  	PassingOnly  bool
    29  	QueryOptions api.QueryOptions
    30  }
    31  
    32  func NewInstancer(o InstancerOptions) sd.Instancer {
    33  	if o.Logger == nil {
    34  		o.Logger = logging.DefaultLogger()
    35  	}
    36  
    37  	i := &instancer{
    38  		client:       o.Client,
    39  		logger:       log.With(o.Logger, "service", o.Service, "tags", fmt.Sprint(o.Tags), "passingOnly", o.PassingOnly, "datacenter", o.QueryOptions.Datacenter),
    40  		service:      o.Service,
    41  		passingOnly:  o.PassingOnly,
    42  		queryOptions: o.QueryOptions,
    43  		stop:         make(chan struct{}),
    44  		registry:     make(map[chan<- sd.Event]bool),
    45  	}
    46  
    47  	if len(o.Tags) > 0 {
    48  		i.tag = o.Tags[0]
    49  		for ix := 1; ix < len(o.Tags); ix++ {
    50  			i.filterTags = append(i.filterTags, o.Tags[ix])
    51  		}
    52  	}
    53  
    54  	// grab the initial set of instances
    55  	instances, index, err := i.getInstances(0, nil)
    56  	if err == nil {
    57  		i.logger.Log(level.Key(), level.InfoValue(), "instances", len(instances))
    58  	} else {
    59  		i.logger.Log(level.Key(), level.ErrorValue(), logging.ErrorKey(), err)
    60  	}
    61  
    62  	i.update(sd.Event{Instances: instances, Err: err})
    63  	go i.loop(index)
    64  
    65  	return i
    66  }
    67  
    68  type instancer struct {
    69  	client  Client
    70  	logger  log.Logger
    71  	service string
    72  
    73  	tag        string
    74  	filterTags []string
    75  
    76  	passingOnly  bool
    77  	queryOptions api.QueryOptions
    78  
    79  	stop chan struct{}
    80  
    81  	registerLock sync.Mutex
    82  	state        sd.Event
    83  	registry     map[chan<- sd.Event]bool
    84  }
    85  
    86  func (i *instancer) update(e sd.Event) {
    87  	sort.Strings(e.Instances)
    88  	defer i.registerLock.Unlock()
    89  	i.registerLock.Lock()
    90  
    91  	if reflect.DeepEqual(i.state, e) {
    92  		return
    93  	}
    94  
    95  	i.state = e
    96  	for c := range i.registry {
    97  		c <- i.state
    98  	}
    99  }
   100  
   101  func (i *instancer) loop(lastIndex uint64) {
   102  	var (
   103  		instances []string
   104  		err       error
   105  		d         time.Duration = 10 * time.Millisecond
   106  	)
   107  
   108  	for {
   109  		instances, lastIndex, err = i.getInstances(lastIndex, i.stop)
   110  		switch {
   111  		case err == errStopped:
   112  			return
   113  
   114  		case err != nil:
   115  			i.logger.Log(logging.ErrorKey(), err)
   116  			time.Sleep(d)
   117  			d = conn.Exponential(d)
   118  			i.update(sd.Event{Err: err})
   119  
   120  		default:
   121  			i.update(sd.Event{Instances: instances})
   122  			d = 10 * time.Millisecond
   123  		}
   124  	}
   125  }
   126  
   127  // getInstances is implemented similarly to go-kits sd/consul version, albeit with support for
   128  // arbitrary query options
   129  func (i *instancer) getInstances(lastIndex uint64, stop <-chan struct{}) ([]string, uint64, error) {
   130  	type response struct {
   131  		instances []string
   132  		index     uint64
   133  		err       error
   134  	}
   135  
   136  	result := make(chan response, 1)
   137  
   138  	go func() {
   139  		var queryOptions api.QueryOptions = i.queryOptions
   140  		queryOptions.WaitIndex = lastIndex
   141  		entries, meta, err := i.client.Service(i.service, i.tag, i.passingOnly, &queryOptions)
   142  		if err != nil {
   143  			result <- response{err: err}
   144  			return
   145  		}
   146  
   147  		if len(i.filterTags) > 0 {
   148  			entries = filterEntries(entries, i.filterTags)
   149  		}
   150  
   151  		// see: https://www.consul.io/api-docs/features/blocking#implementation-details
   152  		if meta == nil || meta.LastIndex < lastIndex {
   153  			lastIndex = 0
   154  		} else {
   155  			lastIndex = meta.LastIndex
   156  		}
   157  
   158  		result <- response{
   159  			instances: makeInstances(entries),
   160  			index:     lastIndex,
   161  		}
   162  	}()
   163  
   164  	select {
   165  	case r := <-result:
   166  		return r.instances, r.index, r.err
   167  	case <-stop:
   168  		return nil, 0, errStopped
   169  	}
   170  }
   171  
   172  func filterEntry(candidate *api.ServiceEntry, requiredTags []string) bool {
   173  	serviceTags := make(map[string]bool, len(candidate.Service.Tags))
   174  	for _, tag := range candidate.Service.Tags {
   175  		serviceTags[tag] = true
   176  	}
   177  
   178  	for _, requiredTag := range requiredTags {
   179  		if !serviceTags[requiredTag] {
   180  			return false
   181  		}
   182  	}
   183  
   184  	return true
   185  }
   186  
   187  // filterEntries is similar to go-kit's version: since consul does not support multiple tags
   188  // in blocking queries, we have to filter manually for multiple tags.
   189  func filterEntries(entries []*api.ServiceEntry, requiredTags []string) []*api.ServiceEntry {
   190  	var filtered []*api.ServiceEntry
   191  	for _, entry := range entries {
   192  		if filterEntry(entry, requiredTags) {
   193  			filtered = append(filtered, entry)
   194  		}
   195  	}
   196  
   197  	return filtered
   198  }
   199  
   200  // makeInstances is identical to go-kit's version
   201  func makeInstances(entries []*api.ServiceEntry) []string {
   202  	instances := make([]string, len(entries))
   203  	for i, entry := range entries {
   204  		address := entry.Node.Address
   205  		if len(entry.Service.Address) > 0 {
   206  			address = entry.Service.Address
   207  		}
   208  
   209  		instances[i] = fmt.Sprintf("%s:%d", address, entry.Service.Port)
   210  	}
   211  
   212  	return instances
   213  }
   214  
   215  func (i *instancer) Register(ch chan<- sd.Event) {
   216  	defer i.registerLock.Unlock()
   217  	i.registerLock.Lock()
   218  	i.registry[ch] = true
   219  
   220  	// push the current state to the new channel
   221  	ch <- i.state
   222  }
   223  
   224  func (i *instancer) Deregister(ch chan<- sd.Event) {
   225  	defer i.registerLock.Unlock()
   226  	i.registerLock.Lock()
   227  	delete(i.registry, ch)
   228  }
   229  
   230  func (i *instancer) Stop() {
   231  	// this isn't idempotent, but mimics go-kit's behavior
   232  	close(i.stop)
   233  }