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 }