github.com/mailgun/holster/v4@v4.20.0/discovery/consul.go (about) 1 package discovery 2 3 import ( 4 "context" 5 "fmt" 6 "reflect" 7 "sort" 8 "time" 9 10 "github.com/hashicorp/consul/api" 11 "github.com/hashicorp/consul/api/watch" 12 "github.com/mailgun/holster/v4/cancel" 13 "github.com/mailgun/holster/v4/consul" 14 "github.com/mailgun/holster/v4/errors" 15 "github.com/mailgun/holster/v4/setter" 16 "github.com/mailgun/holster/v4/syncutil" 17 "github.com/sirupsen/logrus" 18 ) 19 20 type ConsulConfig struct { 21 // This is the consul client config; typically created by calling api.DefaultConfig() 22 ClientConfig *api.Config 23 24 // The name of the catalog we should register under; should be common to all peers in the catalog 25 CatalogName string 26 27 // Information about this peer which should be shared with all other peers in the catalog 28 Peer Peer 29 30 // This is an address the will be registered with consul so it can preform liveliness checks 31 LivelinessAddress string 32 33 // A callback function which is called when the member list changes 34 OnUpdate OnUpdateFunc 35 36 // An interface through which logging will occur; usually *logrus.Entry 37 Logger logrus.FieldLogger 38 } 39 40 type Consul struct { 41 wg syncutil.WaitGroup 42 log logrus.FieldLogger 43 client *api.Client 44 plan *watch.Plan 45 conf *ConsulConfig 46 ctx cancel.Context 47 } 48 49 func NewConsul(conf *ConsulConfig) (Members, error) { 50 setter.SetDefault(&conf.Logger, logrus.WithField("category", "consul-catalog")) 51 setter.SetDefault(&conf.ClientConfig, api.DefaultConfig()) 52 var err error 53 54 if conf.Peer.ID == "" { 55 return nil, errors.New("Peer.ID cannot be empty") 56 } 57 58 if conf.CatalogName == "" { 59 return nil, errors.New("CatalogName cannot be empty") 60 } 61 62 cs := Consul{ 63 ctx: cancel.New(context.Background()), 64 log: conf.Logger, 65 conf: conf, 66 } 67 68 cs.client, err = api.NewClient(cs.conf.ClientConfig) 69 if err != nil { 70 return nil, errors.Wrap(err, "while creating a new client") 71 } 72 73 // Register ourselves in consul as a member of the cluster 74 err = cs.client.Agent().ServiceRegisterOpts(&api.AgentServiceRegistration{ 75 Name: conf.CatalogName, 76 ID: conf.Peer.ID, 77 Tags: []string{"scout-bloom"}, 78 Address: conf.LivelinessAddress, 79 Check: &api.AgentServiceCheck{ 80 DeregisterCriticalServiceAfter: "10m", 81 TTL: "10s", 82 }, 83 Meta: map[string]string{ 84 "peer": string(conf.Peer.Metadata), 85 }, 86 }, api.ServiceRegisterOpts{ReplaceExistingChecks: true}) 87 if err != nil { 88 return nil, errors.Wrapf(err, "while registering the peer '%s' to the service catalog '%s'", 89 conf.Peer.ID, cs.conf.CatalogName) 90 } 91 92 // Update the service check TTL 93 err = cs.client.Agent().UpdateTTL(fmt.Sprintf("service:%s", conf.Peer.ID), "", api.HealthPassing) 94 if err != nil { 95 return nil, errors.Wrap(err, "while updating service TTL after registration") 96 } 97 98 cs.log.Debugf("Registered '%s' with consul catalog '%s'", conf.Peer.ID, conf.CatalogName) 99 100 // Periodically update the TTL check on the registered service 101 ticker := time.NewTicker(time.Second * 4) 102 cs.wg.Until(func(done chan struct{}) bool { 103 select { 104 case <-ticker.C: 105 err := cs.client.Agent().UpdateTTL(fmt.Sprintf("service:%s", conf.Peer.ID), "", api.HealthPassing) 106 if err != nil { 107 cs.log.WithError(err).Warn("while updating consul TTL") 108 } 109 case <-done: 110 ticker.Stop() 111 return false 112 } 113 return true 114 }) 115 116 // Watch for changes to the service list and partition config changes 117 if err := cs.watch(); err != nil { 118 return nil, err 119 } 120 121 return &cs, nil 122 } 123 124 func (cs *Consul) watch() error { 125 changeCh := make(chan []*api.ServiceEntry, 100) 126 var previousPeers map[string]Peer 127 var err error 128 129 cs.plan, err = watch.Parse(map[string]interface{}{ 130 "type": "service", 131 "service": cs.conf.CatalogName, 132 }) 133 if err != nil { 134 return fmt.Errorf("while creating watch plan: %s", err) 135 } 136 137 cs.plan.HybridHandler = func(blockParamVal watch.BlockingParamVal, raw interface{}) { 138 if raw == nil { 139 cs.log.Info("Raw == nil") 140 } 141 if v, ok := raw.([]*api.ServiceEntry); ok && v != nil { 142 changeCh <- v 143 } 144 } 145 146 allChecksPassing := func(checks api.HealthChecks) bool { 147 for _, c := range checks { 148 if c.Status != "passing" { 149 return false 150 } 151 } 152 return true 153 } 154 155 cs.wg.Go(func() { 156 if err := cs.plan.RunWithClientAndHclog(cs.client, consul.NewHCLogAdapter(cs.log, "consul-store")); err != nil { 157 cs.log.WithError(err).Error("Service watch failed") 158 } 159 }) 160 161 cs.wg.Until(func(done chan struct{}) bool { 162 select { 163 case <-done: 164 return false 165 case serviceEntries := <-changeCh: 166 if cs.conf.OnUpdate == nil { 167 return true 168 } 169 peers := make(map[string]Peer) 170 for _, se := range serviceEntries { 171 if !allChecksPassing(se.Checks) { 172 break 173 } 174 meta, ok := se.Service.Meta["peer"] 175 if !ok { 176 cs.log.Errorf("service entry missing 'peer' metadata '%s'", se.Service.ID) 177 } 178 p := Peer{ID: se.Service.ID, Metadata: []byte(meta)} 179 if meta == string(cs.conf.Peer.Metadata) { 180 p.IsSelf = true 181 } 182 peers[p.ID] = p 183 } 184 185 if !reflect.DeepEqual(previousPeers, peers) { 186 var result []Peer 187 for _, v := range peers { 188 result = append(result, v) 189 } 190 // Sort the results to make it easy to compare peer lists 191 sort.Slice(result, func(i, j int) bool { 192 return result[i].ID < result[j].ID 193 }) 194 cs.conf.OnUpdate(result) 195 previousPeers = peers 196 } 197 } 198 return true 199 }) 200 return nil 201 } 202 203 func (cs *Consul) GetPeers(ctx context.Context) ([]Peer, error) { 204 opts := &api.QueryOptions{LocalOnly: true} 205 services, _, err := cs.client.Health().Service(cs.conf.CatalogName, "", true, opts.WithContext(ctx)) 206 if err != nil { 207 return nil, errors.Wrap(err, "while fetching healthy catalog listing") 208 } 209 var peers []Peer 210 for _, i := range services { 211 v, ok := i.Service.Meta["peer"] 212 if !ok { 213 return nil, fmt.Errorf("service entry missing 'peer' metadata '%s'", i.Service.ID) 214 } 215 var p Peer 216 p.Metadata = []byte(v) 217 p.ID = i.Service.ID 218 if v == string(cs.conf.Peer.Metadata) { 219 p.IsSelf = true 220 } 221 peers = append(peers, p) 222 } 223 return peers, nil 224 } 225 226 func (cs *Consul) Close(ctx context.Context) error { 227 errCh := make(chan error) 228 go func() { 229 cs.plan.Stop() 230 cs.wg.Stop() 231 errCh <- cs.client.Agent().ServiceDeregister(cs.conf.Peer.ID) 232 }() 233 234 select { 235 case <-ctx.Done(): 236 cs.ctx.Cancel() 237 return ctx.Err() 238 case err := <-errCh: 239 return err 240 } 241 }