github.com/blend/go-sdk@v1.20220411.3/envoyutil/wait.go (about)

     1  /*
     2  
     3  Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file.
     5  
     6  */
     7  
     8  package envoyutil
     9  
    10  import (
    11  	"context"
    12  	"fmt"
    13  	"io"
    14  	"net/http"
    15  	"strings"
    16  	"sync/atomic"
    17  	"time"
    18  
    19  	"github.com/blend/go-sdk/env"
    20  	"github.com/blend/go-sdk/ex"
    21  	"github.com/blend/go-sdk/logger"
    22  	"github.com/blend/go-sdk/retry"
    23  )
    24  
    25  // NOTE: Ensure that
    26  //       - `http.Client` satisfies `HTTPGetClient`
    27  //       - `WaitForAdmin.executeOnce` satisfies `retry.Action`
    28  var (
    29  	_ HTTPGetClient      = (*http.Client)(nil)
    30  	_ retry.ActionerFunc = (*WaitForAdmin)(nil).executeOnce
    31  )
    32  
    33  var (
    34  	// ErrFailedAttempt is an error class returned when Envoy fails to be
    35  	// ready on a single attempt.
    36  	ErrFailedAttempt = ex.Class("Envoy not yet ready")
    37  	// ErrTimedOut is an error class returned when Envoy fails to be ready
    38  	// after exhausting all attempts.
    39  	ErrTimedOut = ex.Class("Timed out waiting for Envoy to be ready")
    40  )
    41  
    42  const (
    43  	// EnvVarWaitFlag is an environment variable which specifies whether
    44  	// a wait function should wait for the Envoy Admin API to be ready.
    45  	EnvVarWaitFlag = "WAIT_FOR_ENVOY"
    46  	// EnvVarAdminPort is an environment variable which provides an override
    47  	// for the Envoy Admin API port.
    48  	EnvVarAdminPort = "ENVOY_ADMIN_PORT"
    49  	// DefaultAdminPort is the default port used for the Envoy Admin API.
    50  	DefaultAdminPort = "15000"
    51  	// EnumStateLive is a `envoy.admin.v3.ServerInfo.State` value indicating
    52  	// the Envoy server is LIVE. Other possible values of this enum are
    53  	// DRAINING, PRE_INITIALIZING and INITIALIZING, but they are not used
    54  	// here.
    55  	// See: https://github.com/envoyproxy/envoy/blob/b867a4dfae32e600ea0a4087dc7925ded5e2ab2a/api/envoy/admin/v3/server_info.proto#L24-L36
    56  	EnumStateLive = "LIVE"
    57  )
    58  
    59  // HTTPGetClient captures a small part of the `http.Client` interface needed
    60  // to execute a GET request.
    61  type HTTPGetClient interface {
    62  	Get(url string) (resp *http.Response, err error)
    63  }
    64  
    65  // WaitForAdmin encapsulates the settings needed to wait until the Envoy Admin
    66  // API is ready.
    67  type WaitForAdmin struct {
    68  	// Port is the port (on localhost) where the Envoy Admin API is running.
    69  	Port string
    70  	// Sleep is the amount of time to sleep in between failed liveness
    71  	// checks for the Envoy API.
    72  	Sleep time.Duration
    73  	// HTTPClient is the HTTP client to use when sending requests.
    74  	HTTPClient HTTPGetClient
    75  	// Log is an optional logger to be used when executing.
    76  	Log logger.Log
    77  	// Attempt is a counter for the number of attempts that have been made
    78  	// to `executeOnce()`. This makes no attempt at "resetting" or guarding
    79  	// against concurrent usage or re-usage of a `WaitForAdmin` struct.
    80  	Attempt uint32
    81  }
    82  
    83  // IsReady makes a single request to the Envoy Admin API and checks if
    84  // the status is ready.
    85  func (wfa *WaitForAdmin) IsReady() bool {
    86  	readyURL := fmt.Sprintf("http://localhost:%s/ready", wfa.Port)
    87  	resp, err := wfa.HTTPClient.Get(readyURL)
    88  	if err != nil {
    89  		logger.MaybeDebugf(wfa.Log, "Envoy is not ready; connection failed: %s", err)
    90  		return false
    91  	}
    92  
    93  	defer resp.Body.Close()
    94  	body, err := io.ReadAll(resp.Body)
    95  	if err != nil {
    96  		logger.MaybeDebug(wfa.Log, "Envoy is not ready; failed to read response body")
    97  		return false
    98  	}
    99  
   100  	if resp.StatusCode != http.StatusOK {
   101  		logger.MaybeDebugf(wfa.Log, "Envoy is not ready; response status code: %d", resp.StatusCode)
   102  		return false
   103  	}
   104  
   105  	if string(body) != EnumStateLive+"\n" {
   106  		logger.MaybeDebugf(wfa.Log, "Envoy is not ready; response body: %q", string(body))
   107  		return false
   108  	}
   109  
   110  	return true
   111  }
   112  
   113  func (wfa *WaitForAdmin) executeOnce(_ context.Context, _ interface{}) (interface{}, error) {
   114  	attempt := atomic.AddUint32(&wfa.Attempt, 1)
   115  	logger.MaybeDebugf(wfa.Log, "Checking if Envoy is ready, attempt %d", attempt)
   116  	if wfa.IsReady() {
   117  		logger.MaybeDebug(wfa.Log, "Envoy is ready")
   118  		return nil, nil
   119  	}
   120  
   121  	logger.MaybeDebugf(wfa.Log, "Envoy is not yet ready, sleeping for %s", wfa.Sleep)
   122  	return nil, ErrFailedAttempt
   123  }
   124  
   125  // Execute will communicate with the Envoy admin port running on `localhost`,
   126  // which defaults to 15000 but can be overridden with `ENVOY_ADMIN_PORT`. It
   127  // will send `GET /ready` up to 10 times, sleeping for `wfa.Sleep` in between
   128  // if the response is not 200 OK with a body of `LIVE\n`.
   129  func (wfa *WaitForAdmin) Execute(ctx context.Context) error {
   130  	_, err := retry.Retry(
   131  		ctx,
   132  		retry.ActionerFunc(wfa.executeOnce),
   133  		nil,
   134  		retry.OptConstantDelay(wfa.Sleep),
   135  		retry.OptMaxAttempts(10),
   136  	)
   137  	if ex.Is(err, ErrFailedAttempt) {
   138  		return ex.New(ErrTimedOut)
   139  	}
   140  	return err
   141  }
   142  
   143  // MaybeWaitForAdmin will check if Envoy is running if the `WAIT_FOR_ENVOY`
   144  // environment variable is set. This will communicate with the Envoy admin
   145  // port running on `localhost`, which defaults to 15000 but can be overridden
   146  // with `ENVOY_ADMIN_PORT`. It will send `GET /ready` up to 10 times, sleeping
   147  // for 1 second in between if the response is not 200 OK with a body of
   148  // `LIVE\n`.
   149  func MaybeWaitForAdmin(log logger.Log) error {
   150  	if !strings.EqualFold(env.Env()[EnvVarWaitFlag], "true") {
   151  		return nil
   152  	}
   153  
   154  	hc := &http.Client{Timeout: time.Second}
   155  	wfa := WaitForAdmin{
   156  		Port:       env.Env().String(EnvVarAdminPort, DefaultAdminPort),
   157  		Sleep:      time.Second,
   158  		HTTPClient: hc,
   159  		Log:        log,
   160  		Attempt:    0,
   161  	}
   162  
   163  	ctx := context.Background()
   164  	return wfa.Execute(ctx)
   165  }