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  }