refactor: Deflake and Try package

- feat: add CI multiplier
- refactor: readability
- feat: custom Sleep function
- refactor(integration): use custom Sleep
- feat: show Try progress
- feat(try): try response with status code
- refactor(try): use a dedicate package.
- refactor(integration): Try everywhere
- feat(CI): pass CI env var to Integration Tests.
- refactor(acme): increase timeout.
- feat(acme): show Traefik logs
- refactor(integration): use `http.StatusXXX`
- refactor: remove Sleep
This commit is contained in:
Fernandez Ludovic 2017-05-17 15:22:44 +02:00 committed by Ludovic Fernandez
parent ff3481f06b
commit 2610023131
25 changed files with 751 additions and 613 deletions

View file

@ -0,0 +1,95 @@
package try
import (
"errors"
"fmt"
"io/ioutil"
"net/http"
"strings"
"github.com/docker/libkv/store"
)
// ResponseCondition is a retry condition function.
// It receives a response, and returns an error
// if the response failed the condition.
type ResponseCondition func(*http.Response) error
// BodyContains returns a retry condition function.
// The condition returns an error if the request body does not contain all the given
// strings.
func BodyContains(values ...string) ResponseCondition {
return func(res *http.Response) error {
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %s", err)
}
for _, value := range values {
if !strings.Contains(string(body), value) {
return fmt.Errorf("could not find '%s' in body '%s'", value, string(body))
}
}
return nil
}
}
// BodyContainsOr returns a retry condition function.
// The condition returns an error if the request body does not contain one of the given
// strings.
func BodyContainsOr(values ...string) ResponseCondition {
return func(res *http.Response) error {
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %s", err)
}
for _, value := range values {
if strings.Contains(string(body), value) {
return nil
}
}
return fmt.Errorf("could not find '%v' in body '%s'", values, string(body))
}
}
// HasBody returns a retry condition function.
// The condition returns an error if the request body does not have body content.
func HasBody() ResponseCondition {
return func(res *http.Response) error {
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %s", err)
}
if len(body) == 0 {
return errors.New("Response doesn't have body content")
}
return nil
}
}
// StatusCodeIs returns a retry condition function.
// The condition returns an error if the given response's status code is not the
// given HTTP status code.
func StatusCodeIs(status int) ResponseCondition {
return func(res *http.Response) error {
if res.StatusCode != status {
return fmt.Errorf("got status code %d, wanted %d", res.StatusCode, status)
}
return nil
}
}
// DoCondition is a retry condition function.
// It returns an error
type DoCondition func() error
// KVExists is a retry condition function.
// Verify if a Key exists in the store
func KVExists(kv store.Store, key string) DoCondition {
return func() error {
_, err := kv.Exists(key)
return err
}
}

156
integration/try/try.go Normal file
View file

@ -0,0 +1,156 @@
package try
import (
"fmt"
"math"
"net/http"
"os"
"time"
"github.com/containous/traefik/log"
)
const (
// CITimeoutMultiplier is the multiplier for all timeout in the CI
CITimeoutMultiplier = 3
maxInterval = 5 * time.Second
)
type timedAction func(timeout time.Duration, operation DoCondition) error
// Sleep pauses the current goroutine for at least the duration d.
// Deprecated: Use only when use an other Try[...] functions is not possible.
func Sleep(d time.Duration) {
d = applyCIMultiplier(d)
time.Sleep(d)
}
// Response is like Request, but returns the response for further
// processing at the call site.
// Conditions are not allowed since it would complicate signaling if the
// response body needs to be closed or not. Callers are expected to close on
// their own if the function returns a nil error.
func Response(req *http.Request, timeout time.Duration) (*http.Response, error) {
return doTryRequest(req, timeout)
}
// ResponseUntilStatusCode is like Request, but returns the response for further
// processing at the call site.
// Conditions are not allowed since it would complicate signaling if the
// response body needs to be closed or not. Callers are expected to close on
// their own if the function returns a nil error.
func ResponseUntilStatusCode(req *http.Request, timeout time.Duration, statusCode int) (*http.Response, error) {
return doTryRequest(req, timeout, StatusCodeIs(statusCode))
}
// GetRequest is like Do, but runs a request against the given URL and applies
// the condition on the response.
// ResponseCondition may be nil, in which case only the request against the URL must
// succeed.
func GetRequest(url string, timeout time.Duration, conditions ...ResponseCondition) error {
resp, err := doTryGet(url, timeout, conditions...)
if resp != nil && resp.Body != nil {
defer resp.Body.Close()
}
return err
}
// Request is like Do, but runs a request against the given URL and applies
// the condition on the response.
// ResponseCondition may be nil, in which case only the request against the URL must
// succeed.
func Request(req *http.Request, timeout time.Duration, conditions ...ResponseCondition) error {
resp, err := doTryRequest(req, timeout, conditions...)
if resp != nil && resp.Body != nil {
defer resp.Body.Close()
}
return err
}
// Do repeatedly executes an operation until no error condition occurs or the
// given timeout is reached, whatever comes first.
func Do(timeout time.Duration, operation DoCondition) error {
if timeout <= 0 {
panic("timeout must be larger than zero")
}
interval := time.Duration(math.Ceil(float64(timeout) / 15.0))
if interval > maxInterval {
interval = maxInterval
}
timeout = applyCIMultiplier(timeout)
var err error
if err = operation(); err == nil {
fmt.Println("+")
return nil
}
fmt.Print("*")
stopTimer := time.NewTimer(timeout)
defer stopTimer.Stop()
retryTick := time.NewTicker(interval)
defer retryTick.Stop()
for {
select {
case <-stopTimer.C:
fmt.Println("-")
return fmt.Errorf("try operation failed: %s", err)
case <-retryTick.C:
fmt.Print("*")
if err = operation(); err == nil {
fmt.Println("+")
return err
}
}
}
}
func doTryGet(url string, timeout time.Duration, conditions ...ResponseCondition) (*http.Response, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
return doTryRequest(req, timeout, conditions...)
}
func doTryRequest(request *http.Request, timeout time.Duration, conditions ...ResponseCondition) (*http.Response, error) {
return doRequest(Do, timeout, request, conditions...)
}
func doRequest(action timedAction, timeout time.Duration, request *http.Request, conditions ...ResponseCondition) (*http.Response, error) {
var resp *http.Response
return resp, action(timeout, func() error {
var err error
client := http.DefaultClient
resp, err = client.Do(request)
if err != nil {
return err
}
for _, condition := range conditions {
if err := condition(resp); err != nil {
return err
}
}
return nil
})
}
func applyCIMultiplier(timeout time.Duration) time.Duration {
ci := os.Getenv("CI")
if len(ci) > 0 {
log.Debug("Apply CI multiplier:", CITimeoutMultiplier)
return time.Duration(float64(timeout) * CITimeoutMultiplier)
}
return timeout
}