diff options
author | Niall Sheridan <nsheridan@gmail.com> | 2016-10-04 14:37:01 -0700 |
---|---|---|
committer | Niall Sheridan <nsheridan@gmail.com> | 2016-10-06 22:02:39 -0500 |
commit | 17cd70cea546e287713a3d4c086528a85abefa2e (patch) | |
tree | f52ffa10f2065c47445bd6c37f07a57f68074100 /vendor/github.com/sethgrid/pester/main.go | |
parent | 294020406c257ad4eb1867a1e7fb8b694aefddd2 (diff) |
Add support for Hashicorp Vault
Vault is supported for the following:
As a well-known filesystem for TLS cert, TLS key and SSH signing key.
For configuration secrets for cookie_secret, csrf_secret, oauth_client_id and oauth_client_secret options.
Diffstat (limited to 'vendor/github.com/sethgrid/pester/main.go')
-rw-r--r-- | vendor/github.com/sethgrid/pester/main.go | 423 |
1 files changed, 423 insertions, 0 deletions
diff --git a/vendor/github.com/sethgrid/pester/main.go b/vendor/github.com/sethgrid/pester/main.go new file mode 100644 index 0000000..8eb91fe --- /dev/null +++ b/vendor/github.com/sethgrid/pester/main.go @@ -0,0 +1,423 @@ +package pester + +// pester provides additional resiliency over the standard http client methods by +// allowing you to control concurrency, retries, and a backoff strategy. + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/ioutil" + "math" + "math/rand" + "net/http" + "net/url" + "sync" + "time" +) + +// Client wraps the http client and exposes all the functionality of the http.Client. +// Additionally, Client provides pester specific values for handling resiliency. +type Client struct { + // wrap it to provide access to http built ins + hc *http.Client + + Transport http.RoundTripper + CheckRedirect func(req *http.Request, via []*http.Request) error + Jar http.CookieJar + Timeout time.Duration + + // pester specific + Concurrency int + MaxRetries int + Backoff BackoffStrategy + KeepLog bool + + SuccessReqNum int + SuccessRetryNum int + + wg *sync.WaitGroup + + sync.Mutex + ErrLog []ErrEntry +} + +// ErrEntry is used to provide the LogString() data and is populated +// each time an error happens if KeepLog is set. +// ErrEntry.Retry is deprecated in favor of ErrEntry.Attempt +type ErrEntry struct { + Time time.Time + Method string + URL string + Verb string + Request int + Retry int + Attempt int + Err error +} + +// result simplifies the channel communication for concurrent request handling +type result struct { + resp *http.Response + err error + req int + retry int +} + +// params represents all the params needed to run http client calls and pester errors +type params struct { + method string + verb string + req *http.Request + url string + bodyType string + body io.Reader + data url.Values +} + +// New constructs a new DefaultClient with sensible default values +func New() *Client { + return &Client{ + Concurrency: DefaultClient.Concurrency, + MaxRetries: DefaultClient.MaxRetries, + Backoff: DefaultClient.Backoff, + ErrLog: DefaultClient.ErrLog, + wg: &sync.WaitGroup{}, + } +} + +// NewExtendedClient allows you to pass in an http.Client that is previously set up +// and extends it to have Pester's features of concurrency and retries. +func NewExtendedClient(hc *http.Client) *Client { + c := New() + c.hc = hc + return c +} + +// BackoffStrategy is used to determine how long a retry request should wait until attempted +type BackoffStrategy func(retry int) time.Duration + +// DefaultClient provides sensible defaults +var DefaultClient = &Client{Concurrency: 1, MaxRetries: 3, Backoff: DefaultBackoff, ErrLog: []ErrEntry{}} + +// DefaultBackoff always returns 1 second +func DefaultBackoff(_ int) time.Duration { + return 1 * time.Second +} + +// ExponentialBackoff returns ever increasing backoffs by a power of 2 +func ExponentialBackoff(i int) time.Duration { + return time.Duration(math.Pow(2, float64(i))) * time.Second +} + +// ExponentialJitterBackoff returns ever increasing backoffs by a power of 2 +// with +/- 0-33% to prevent sychronized reuqests. +func ExponentialJitterBackoff(i int) time.Duration { + return jitter(int(math.Pow(2, float64(i)))) +} + +// LinearBackoff returns increasing durations, each a second longer than the last +func LinearBackoff(i int) time.Duration { + return time.Duration(i) * time.Second +} + +// LinearJitterBackoff returns increasing durations, each a second longer than the last +// with +/- 0-33% to prevent sychronized reuqests. +func LinearJitterBackoff(i int) time.Duration { + return jitter(i) +} + +// jitter keeps the +/- 0-33% logic in one place +func jitter(i int) time.Duration { + ms := i * 1000 + + maxJitter := ms / 3 + + rand.Seed(time.Now().Unix()) + jitter := rand.Intn(maxJitter + 1) + + if rand.Intn(2) == 1 { + ms = ms + jitter + } else { + ms = ms - jitter + } + + // a jitter of 0 messes up the time.Tick chan + if ms <= 0 { + ms = 1 + } + + return time.Duration(ms) * time.Millisecond +} + +// Wait blocks until all pester requests have returned +// Probably not that useful outside of testing. +func (c *Client) Wait() { + c.wg.Wait() +} + +// pester provides all the logic of retries, concurrency, backoff, and logging +func (c *Client) pester(p params) (*http.Response, error) { + resultCh := make(chan result) + multiplexCh := make(chan result) + finishCh := make(chan struct{}) + + // track all requests that go out so we can close the late listener routine that closes late incoming response bodies + totalSentRequests := &sync.WaitGroup{} + totalSentRequests.Add(1) + defer totalSentRequests.Done() + allRequestsBackCh := make(chan struct{}) + go func() { + totalSentRequests.Wait() + close(allRequestsBackCh) + }() + + // GET calls should be idempotent and can make use + // of concurrency. Other verbs can mutate and should not + // make use of the concurrency feature + concurrency := c.Concurrency + if p.verb != "GET" { + concurrency = 1 + } + + c.Lock() + if c.hc == nil { + c.hc = &http.Client{} + c.hc.Transport = c.Transport + c.hc.CheckRedirect = c.CheckRedirect + c.hc.Jar = c.Jar + c.hc.Timeout = c.Timeout + } + c.Unlock() + + // re-create the http client so we can leverage the std lib + httpClient := http.Client{ + Transport: c.hc.Transport, + CheckRedirect: c.hc.CheckRedirect, + Jar: c.hc.Jar, + Timeout: c.hc.Timeout, + } + + // if we have a request body, we need to save it for later + var originalRequestBody []byte + var originalBody []byte + var err error + if p.req != nil && p.req.Body != nil { + originalRequestBody, err = ioutil.ReadAll(p.req.Body) + if err != nil { + return &http.Response{}, errors.New("error reading request body") + } + p.req.Body.Close() + } + if p.body != nil { + originalBody, err = ioutil.ReadAll(p.body) + if err != nil { + return &http.Response{}, errors.New("error reading body") + } + } + + AttemptLimit := c.MaxRetries + if AttemptLimit <= 0 { + AttemptLimit = 1 + } + + for req := 0; req < concurrency; req++ { + c.wg.Add(1) + totalSentRequests.Add(1) + go func(n int, p params) { + defer c.wg.Done() + defer totalSentRequests.Done() + + var err error + for i := 1; i <= AttemptLimit; i++ { + c.wg.Add(1) + defer c.wg.Done() + select { + case <-finishCh: + return + default: + } + resp := &http.Response{} + + // rehydrate the body (it is drained each read) + if len(originalRequestBody) > 0 { + p.req.Body = ioutil.NopCloser(bytes.NewBuffer(originalRequestBody)) + } + if len(originalBody) > 0 { + p.body = bytes.NewBuffer(originalBody) + } + + // route the calls + switch p.method { + case "Do": + resp, err = httpClient.Do(p.req) + case "Get": + resp, err = httpClient.Get(p.url) + case "Head": + resp, err = httpClient.Head(p.url) + case "Post": + resp, err = httpClient.Post(p.url, p.bodyType, p.body) + case "PostForm": + resp, err = httpClient.PostForm(p.url, p.data) + } + + // Early return if we have a valid result + // Only retry (ie, continue the loop) on 5xx status codes + if err == nil && resp.StatusCode < 500 { + multiplexCh <- result{resp: resp, err: err, req: n, retry: i} + return + } + + c.log(ErrEntry{ + Time: time.Now(), + Method: p.method, + Verb: p.verb, + URL: p.url, + Request: n, + Retry: i + 1, // would remove, but would break backward compatibility + Attempt: i, + Err: err, + }) + + // if it is the last iteration, grab the result (which is an error at this point) + if i == AttemptLimit { + multiplexCh <- result{resp: resp, err: err} + return + } + + // if we are retrying, we should close this response body to free the fd + if resp != nil { + resp.Body.Close() + } + + // prevent a 0 from causing the tick to block, pass additional microsecond + <-time.Tick(c.Backoff(i) + 1*time.Microsecond) + } + }(req, p) + } + + // spin off the go routine so it can continually listen in on late results and close the response bodies + go func() { + gotFirstResult := false + for { + select { + case res := <-multiplexCh: + if !gotFirstResult { + gotFirstResult = true + close(finishCh) + resultCh <- res + } else if res.resp != nil { + // we only return one result to the caller; close all other response bodies that come back + // drain the body before close as to not prevent keepalive. see https://gist.github.com/mholt/eba0f2cc96658be0f717 + io.Copy(ioutil.Discard, res.resp.Body) + res.resp.Body.Close() + } + case <-allRequestsBackCh: + // don't leave this goroutine running + return + } + } + }() + + select { + case res := <-resultCh: + c.Lock() + defer c.Unlock() + c.SuccessReqNum = res.req + c.SuccessRetryNum = res.retry + return res.resp, res.err + } +} + +// LogString provides a string representation of the errors the client has seen +func (c *Client) LogString() string { + c.Lock() + defer c.Unlock() + var res string + for _, e := range c.ErrLog { + res += fmt.Sprintf("%d %s [%s] %s request-%d retry-%d error: %s\n", + e.Time.Unix(), e.Method, e.Verb, e.URL, e.Request, e.Retry, e.Err) + } + return res +} + +// LogErrCount is a helper method used primarily for test validation +func (c *Client) LogErrCount() int { + c.Lock() + defer c.Unlock() + return len(c.ErrLog) +} + +// EmbedHTTPClient allows you to extend an existing Pester client with an +// underlying http.Client, such as https://godoc.org/golang.org/x/oauth2/google#DefaultClient +func (c *Client) EmbedHTTPClient(hc *http.Client) { + c.hc = hc +} + +func (c *Client) log(e ErrEntry) { + if c.KeepLog { + c.Lock() + c.ErrLog = append(c.ErrLog, e) + c.Unlock() + } +} + +// Do provides the same functionality as http.Client.Do +func (c *Client) Do(req *http.Request) (resp *http.Response, err error) { + return c.pester(params{method: "Do", req: req, verb: req.Method, url: req.URL.String()}) +} + +// Get provides the same functionality as http.Client.Get +func (c *Client) Get(url string) (resp *http.Response, err error) { + return c.pester(params{method: "Get", url: url, verb: "GET"}) +} + +// Head provides the same functionality as http.Client.Head +func (c *Client) Head(url string) (resp *http.Response, err error) { + return c.pester(params{method: "Head", url: url, verb: "HEAD"}) +} + +// Post provides the same functionality as http.Client.Post +func (c *Client) Post(url string, bodyType string, body io.Reader) (resp *http.Response, err error) { + return c.pester(params{method: "Post", url: url, bodyType: bodyType, body: body, verb: "POST"}) +} + +// PostForm provides the same functionality as http.Client.PostForm +func (c *Client) PostForm(url string, data url.Values) (resp *http.Response, err error) { + return c.pester(params{method: "PostForm", url: url, data: data, verb: "POST"}) +} + +//////////////////////////////////////// +// Provide self-constructing variants // +//////////////////////////////////////// + +// Do provides the same functionality as http.Client.Do and creates its own constructor +func Do(req *http.Request) (resp *http.Response, err error) { + c := New() + return c.Do(req) +} + +// Get provides the same functionality as http.Client.Get and creates its own constructor +func Get(url string) (resp *http.Response, err error) { + c := New() + return c.Get(url) +} + +// Head provides the same functionality as http.Client.Head and creates its own constructor +func Head(url string) (resp *http.Response, err error) { + c := New() + return c.Head(url) +} + +// Post provides the same functionality as http.Client.Post and creates its own constructor +func Post(url string, bodyType string, body io.Reader) (resp *http.Response, err error) { + c := New() + return c.Post(url, bodyType, body) +} + +// PostForm provides the same functionality as http.Client.PostForm and creates its own constructor +func PostForm(url string, data url.Values) (resp *http.Response, err error) { + c := New() + return c.PostForm(url, data) +} |