traefik/integration/try/try.go
2024-09-16 16:41:57 +02:00

173 lines
4.9 KiB
Go

package try
import (
"fmt"
"math"
"net/http"
"os"
"time"
"github.com/rs/zerolog/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 another 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, nil)
}
// 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, nil, 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, nil, 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, nil, conditions...)
if resp != nil && resp.Body != nil {
defer resp.Body.Close()
}
return err
}
// RequestWithTransport 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 RequestWithTransport(req *http.Request, timeout time.Duration, transport *http.Transport, conditions ...ResponseCondition) error {
resp, err := doTryRequest(req, timeout, transport, 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: %w", err)
case <-retryTick.C:
fmt.Print("*")
if err = operation(); err == nil {
fmt.Println("+")
return nil
}
}
}
}
func doTryGet(url string, timeout time.Duration, transport http.RoundTripper, conditions ...ResponseCondition) (*http.Response, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
return doTryRequest(req, timeout, transport, conditions...)
}
func doTryRequest(request *http.Request, timeout time.Duration, transport http.RoundTripper, conditions ...ResponseCondition) (*http.Response, error) {
return doRequest(Do, timeout, request, transport, conditions...)
}
func doRequest(action timedAction, timeout time.Duration, request *http.Request, transport http.RoundTripper, conditions ...ResponseCondition) (*http.Response, error) {
var resp *http.Response
return resp, action(timeout, func() error {
var err error
client := http.DefaultClient
if transport != nil {
client.Transport = transport
}
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().Msgf("Apply CI multiplier: %d", CITimeoutMultiplier)
return time.Duration(float64(timeout) * CITimeoutMultiplier)
}
return timeout
}