2017-05-17 13:22:44 +00:00
|
|
|
package try
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"math"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"time"
|
|
|
|
|
2022-11-21 17:36:05 +00:00
|
|
|
"github.com/rs/zerolog/log"
|
2017-05-17 13:22:44 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2020-08-21 09:12:04 +00:00
|
|
|
// CITimeoutMultiplier is the multiplier for all timeout in the CI.
|
2017-05-17 13:22:44 +00:00
|
|
|
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) {
|
2018-07-26 10:42:03 +00:00
|
|
|
return doTryRequest(req, timeout, nil)
|
2017-05-17 13:22:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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) {
|
2018-07-26 10:42:03 +00:00
|
|
|
return doTryRequest(req, timeout, nil, StatusCodeIs(statusCode))
|
2017-05-17 13:22:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
2018-07-26 10:42:03 +00:00
|
|
|
resp, err := doTryGet(url, timeout, nil, conditions...)
|
2017-05-17 13:22:44 +00:00
|
|
|
|
|
|
|
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 {
|
2018-07-26 10:42:03 +00:00
|
|
|
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...)
|
2017-05-17 13:22:44 +00:00
|
|
|
|
|
|
|
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("-")
|
2020-05-11 10:06:07 +00:00
|
|
|
return fmt.Errorf("try operation failed: %w", err)
|
2017-05-17 13:22:44 +00:00
|
|
|
case <-retryTick.C:
|
|
|
|
fmt.Print("*")
|
|
|
|
if err = operation(); err == nil {
|
|
|
|
fmt.Println("+")
|
2021-03-04 08:02:03 +00:00
|
|
|
return nil
|
2017-05-17 13:22:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-04 15:40:05 +00:00
|
|
|
func doTryGet(url string, timeout time.Duration, transport http.RoundTripper, conditions ...ResponseCondition) (*http.Response, error) {
|
2017-05-17 13:22:44 +00:00
|
|
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-07-26 10:42:03 +00:00
|
|
|
return doTryRequest(req, timeout, transport, conditions...)
|
2017-05-17 13:22:44 +00:00
|
|
|
}
|
|
|
|
|
2019-03-04 15:40:05 +00:00
|
|
|
func doTryRequest(request *http.Request, timeout time.Duration, transport http.RoundTripper, conditions ...ResponseCondition) (*http.Response, error) {
|
2018-07-26 10:42:03 +00:00
|
|
|
return doRequest(Do, timeout, request, transport, conditions...)
|
2017-05-17 13:22:44 +00:00
|
|
|
}
|
|
|
|
|
2019-03-04 15:40:05 +00:00
|
|
|
func doRequest(action timedAction, timeout time.Duration, request *http.Request, transport http.RoundTripper, conditions ...ResponseCondition) (*http.Response, error) {
|
2017-05-17 13:22:44 +00:00
|
|
|
var resp *http.Response
|
|
|
|
return resp, action(timeout, func() error {
|
|
|
|
var err error
|
|
|
|
client := http.DefaultClient
|
2018-07-26 10:42:03 +00:00
|
|
|
if transport != nil {
|
|
|
|
client.Transport = transport
|
|
|
|
}
|
2017-05-17 13:22:44 +00:00
|
|
|
|
|
|
|
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 {
|
2022-11-21 17:36:05 +00:00
|
|
|
log.Debug().Msgf("Apply CI multiplier: %d", CITimeoutMultiplier)
|
2017-05-17 13:22:44 +00:00
|
|
|
return time.Duration(float64(timeout) * CITimeoutMultiplier)
|
|
|
|
}
|
|
|
|
return timeout
|
|
|
|
}
|