hub.fastgit.org/hashicorp/consul.git@v1.4.5/command/connect/proxy/register.go (about) 1 package proxy 2 3 import ( 4 "fmt" 5 "log" 6 "os" 7 "strings" 8 "sync" 9 "time" 10 11 "github.com/hashicorp/consul/api" 12 ) 13 14 const ( 15 // RegisterReconcilePeriod is how often the monitor will attempt to 16 // reconcile the expected service state with the remote Consul server. 17 RegisterReconcilePeriod = 30 * time.Second 18 19 // RegisterTTLPeriod is the TTL setting for the health check of the 20 // service. The monitor will automatically pass the health check 21 // three times per this period to be more resilient to failures. 22 RegisterTTLPeriod = 30 * time.Second 23 ) 24 25 // RegisterMonitor registers the proxy with the local Consul agent with a TTL 26 // health check that is kept alive. 27 // 28 // This struct should be intialized with NewRegisterMonitor instead of being 29 // allocated directly. Using this struct without calling NewRegisterMonitor 30 // will result in panics. 31 type RegisterMonitor struct { 32 // Logger is the logger for the monitor. 33 Logger *log.Logger 34 35 // Client is the API client to a specific Consul agent. This agent is 36 // where the service will be registered. 37 Client *api.Client 38 39 // Service is the name of the service being proxied. 40 Service string 41 42 // LocalAddress and LocalPort are the address and port of the proxy 43 // itself, NOT the service being proxied. 44 LocalAddress string 45 LocalPort int 46 47 // IDSuffix is a unique ID that is appended to the end of the service 48 // name. This helps the service be unique. By default the service ID 49 // is just the proxied service name followed by "-proxy". 50 IDSuffix string 51 52 // The fields below are related to timing settings. See the default 53 // constants for more documentation on what they set. 54 ReconcilePeriod time.Duration 55 TTLPeriod time.Duration 56 57 // lock is held while reading/writing any internal state of the monitor. 58 // cond is a condition variable on lock that is broadcasted for runState 59 // changes. 60 lock *sync.Mutex 61 cond *sync.Cond 62 63 // runState is the current state of the monitor. To read this the 64 // lock must be held. The condition variable cond can be waited on 65 // for changes to this value. 66 runState registerRunState 67 } 68 69 // registerState is the state of the RegisterMonitor. 70 // 71 // This is a basic state machine with the following transitions: 72 // 73 // * idle => running, stopped 74 // * running => stopping, stopped 75 // * stopping => stopped 76 // * stopped => <> 77 // 78 type registerRunState uint8 79 80 const ( 81 registerStateIdle registerRunState = iota 82 registerStateRunning 83 registerStateStopping 84 registerStateStopped 85 ) 86 87 // NewRegisterMonitor initializes a RegisterMonitor. After initialization, 88 // the exported fields should be configured as desired. To start the monitor, 89 // execute Run in a goroutine. 90 func NewRegisterMonitor() *RegisterMonitor { 91 var lock sync.Mutex 92 return &RegisterMonitor{ 93 Logger: log.New(os.Stderr, "", log.LstdFlags), // default logger 94 ReconcilePeriod: RegisterReconcilePeriod, 95 TTLPeriod: RegisterTTLPeriod, 96 lock: &lock, 97 cond: sync.NewCond(&lock), 98 } 99 } 100 101 // Run should be started in a goroutine and will keep Consul updated 102 // in the background with the state of this proxy. If registration fails 103 // this will continue to retry. 104 func (r *RegisterMonitor) Run() { 105 // Grab the lock and set our state. If we're not idle, then we return 106 // immediately since the monitor is only allowed to run once. 107 r.lock.Lock() 108 if r.runState != registerStateIdle { 109 r.lock.Unlock() 110 return 111 } 112 r.runState = registerStateRunning 113 r.lock.Unlock() 114 115 // Start a goroutine that just waits for a stop request 116 stopCh := make(chan struct{}) 117 go func() { 118 defer close(stopCh) 119 r.lock.Lock() 120 defer r.lock.Unlock() 121 122 // We wait for anything not running, just so we're more resilient 123 // in the face of state machine issues. Basically any state change 124 // will cause us to quit. 125 for r.runState == registerStateRunning { 126 r.cond.Wait() 127 } 128 }() 129 130 // When we exit, we set the state to stopped and broadcast to any 131 // waiting Close functions that they can return. 132 defer func() { 133 r.lock.Lock() 134 r.runState = registerStateStopped 135 r.cond.Broadcast() 136 r.lock.Unlock() 137 }() 138 139 // Run the first registration optimistically. If this fails then its 140 // okay since we'll just retry shortly. 141 r.register() 142 143 // Create the timers for trigger events. We don't use tickers because 144 // we don't want the events to pile on. 145 reconcileTimer := time.NewTimer(r.ReconcilePeriod) 146 heartbeatTimer := time.NewTimer(r.TTLPeriod / 3) 147 148 for { 149 select { 150 case <-reconcileTimer.C: 151 r.register() 152 reconcileTimer.Reset(r.ReconcilePeriod) 153 154 case <-heartbeatTimer.C: 155 r.heartbeat() 156 heartbeatTimer.Reset(r.TTLPeriod / 3) 157 158 case <-stopCh: 159 r.Logger.Printf("[INFO] proxy: stop request received, deregistering") 160 r.deregister() 161 return 162 } 163 } 164 } 165 166 // register queries the Consul agent to determine if we've already registered. 167 // If we haven't or the registered service differs from what we're trying to 168 // register, then we attempt to register our service. 169 func (r *RegisterMonitor) register() { 170 catalog := r.Client.Catalog() 171 serviceID := r.serviceID() 172 serviceName := r.serviceName() 173 174 // Determine the current state of this service in Consul 175 var currentService *api.CatalogService 176 services, _, err := catalog.Service( 177 serviceName, "", 178 &api.QueryOptions{AllowStale: true}) 179 if err == nil { 180 for _, service := range services { 181 if serviceID == service.ServiceID { 182 currentService = service 183 break 184 } 185 } 186 } 187 188 // If we have a matching service, then we verify if we need to reregister 189 // by comparing if it matches what we expect. 190 if currentService != nil && 191 currentService.ServiceAddress == r.LocalAddress && 192 currentService.ServicePort == r.LocalPort { 193 r.Logger.Printf("[DEBUG] proxy: service already registered, not re-registering") 194 return 195 } 196 197 // If we're here, then we're registering the service. 198 err = r.Client.Agent().ServiceRegister(&api.AgentServiceRegistration{ 199 Kind: api.ServiceKindConnectProxy, 200 ID: serviceID, 201 Name: serviceName, 202 Address: r.LocalAddress, 203 Port: r.LocalPort, 204 Proxy: &api.AgentServiceConnectProxyConfig{ 205 DestinationServiceName: r.Service, 206 }, 207 Check: &api.AgentServiceCheck{ 208 CheckID: r.checkID(), 209 Name: "proxy heartbeat", 210 TTL: "30s", 211 Notes: "Built-in proxy will heartbeat this check.", 212 Status: "passing", 213 }, 214 }) 215 if err != nil { 216 r.Logger.Printf("[WARN] proxy: Failed to register Consul service: %s", err) 217 return 218 } 219 220 r.Logger.Printf("[INFO] proxy: registered Consul service: %s", serviceID) 221 } 222 223 // heartbeat just pings the TTL check for our service. 224 func (r *RegisterMonitor) heartbeat() { 225 // Trigger the health check passing. We don't need to retry this 226 // since we do a couple tries within the TTL period. 227 if err := r.Client.Agent().PassTTL(r.checkID(), ""); err != nil { 228 if !strings.Contains(err.Error(), "does not have associated") { 229 r.Logger.Printf("[WARN] proxy: heartbeat failed: %s", err) 230 } 231 } 232 } 233 234 // deregister deregisters the service. 235 func (r *RegisterMonitor) deregister() { 236 // Basic retry loop, no backoff for now. But we want to retry a few 237 // times just in case there are basic ephemeral issues. 238 for i := 0; i < 3; i++ { 239 err := r.Client.Agent().ServiceDeregister(r.serviceID()) 240 if err == nil { 241 return 242 } 243 244 r.Logger.Printf("[WARN] proxy: service deregister failed: %s", err) 245 time.Sleep(500 * time.Millisecond) 246 } 247 } 248 249 // Close stops the register goroutines and deregisters the service. Once 250 // Close is called, the monitor can no longer be used again. It is safe to 251 // call Close multiple times and concurrently. 252 func (r *RegisterMonitor) Close() error { 253 r.lock.Lock() 254 defer r.lock.Unlock() 255 256 for { 257 switch r.runState { 258 case registerStateIdle: 259 // Idle so just set it to stopped and return. We notify 260 // the condition variable in case others are waiting. 261 r.runState = registerStateStopped 262 r.cond.Broadcast() 263 return nil 264 265 case registerStateRunning: 266 // Set the state to stopping and broadcast to all waiters, 267 // since Run is sitting on cond.Wait. 268 r.runState = registerStateStopping 269 r.cond.Broadcast() 270 r.cond.Wait() // Wait on the stopping event 271 272 case registerStateStopping: 273 // Still stopping, wait... 274 r.cond.Wait() 275 276 case registerStateStopped: 277 // Stopped, target state reached 278 return nil 279 } 280 } 281 } 282 283 // serviceID returns the unique ID for this proxy service. 284 func (r *RegisterMonitor) serviceID() string { 285 id := fmt.Sprintf("%s-proxy", r.Service) 286 if r.IDSuffix != "" { 287 id += "-" + r.IDSuffix 288 } 289 290 return id 291 } 292 293 // serviceName returns the non-unique name of this proxy service. 294 func (r *RegisterMonitor) serviceName() string { 295 return fmt.Sprintf("%s-proxy", r.Service) 296 } 297 298 // checkID is the unique ID for the registered health check. 299 func (r *RegisterMonitor) checkID() string { 300 return fmt.Sprintf("%s-ttl", r.serviceID()) 301 }