20831: Add DiscoveryDocument to arvados.API
[arvados.git] / sdk / go / arvados / client.go
index 1ccf187ea69b26c45d3d088cc7145dc4e87a7f93..735a44d24c9263d3248fd5c9ae2b0574ca15ca22 100644 (file)
@@ -16,12 +16,15 @@ import (
        "io/fs"
        "io/ioutil"
        "log"
+       "math"
        "math/big"
+       mathrand "math/rand"
        "net"
        "net/http"
        "net/url"
        "os"
        "regexp"
+       "strconv"
        "strings"
        "sync/atomic"
        "time"
@@ -142,11 +145,12 @@ func NewClientFromConfig(cluster *Cluster) (*Client, error) {
                }
        }
        return &Client{
-               Client:   hc,
-               Scheme:   ctrlURL.Scheme,
-               APIHost:  ctrlURL.Host,
-               Insecure: cluster.TLS.Insecure,
-               Timeout:  5 * time.Minute,
+               Client:         hc,
+               Scheme:         ctrlURL.Scheme,
+               APIHost:        ctrlURL.Host,
+               Insecure:       cluster.TLS.Insecure,
+               Timeout:        5 * time.Minute,
+               requestLimiter: requestLimiter{maxlimit: int64(cluster.API.MaxConcurrentRequests / 4)},
        }, nil
 }
 
@@ -230,44 +234,10 @@ func NewClientFromEnv() *Client {
        }
 }
 
-func shouldRetry(req *http.Request, resp *http.Response, err error) bool {
-       if nerr := net.Error(nil); errors.As(err, &nerr) && nerr.Temporary() {
-               return true
-       }
-       switch req.Method {
-       case "GET", "HEAD", "PUT", "OPTIONS", "DELETE":
-       default:
-               return false
-       }
-       if uerr := new(url.Error); errors.As(err, &uerr) && uerr.Err.Error() == "Service Unavailable" {
-               // This is how http.Client reports 503 from proxy server
-               return true
-       }
-       if err != nil {
-               return false
-       }
-       switch resp.StatusCode {
-       case 408, 409, 422, 423, 500, 502, 503, 504:
-               return true
-       default:
-               return false
-       }
-}
-
 var reqIDGen = httpserver.IDGenerator{Prefix: "req-"}
 
 var nopCancelFunc context.CancelFunc = func() {}
 
-func (c *Client) checkRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {
-       if c.requestLimiter.Report(resp, err) {
-               c.last503.Store(time.Now())
-       }
-       if c.Timeout == 0 {
-               return false, err
-       }
-       return retryablehttp.DefaultRetryPolicy(ctx, resp, err)
-}
-
 // Do augments (*http.Client)Do(): adds Authorization and X-Request-Id
 // headers, delays in order to comply with rate-limiting restrictions,
 // and retries failed requests when appropriate.
@@ -307,6 +277,7 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
 
        rclient := retryablehttp.NewClient()
        rclient.HTTPClient = c.httpClient()
+       rclient.Backoff = exponentialBackoff
        if c.Timeout > 0 {
                rclient.RetryWaitMax = c.Timeout / 10
                rclient.RetryMax = 32
@@ -403,6 +374,40 @@ func isRedirectStatus(code int) bool {
        }
 }
 
+const minExponentialBackoffBase = time.Second
+
+// Implements retryablehttp.Backoff using the server-provided
+// Retry-After header if available, otherwise nearly-full jitter
+// exponential backoff (similar to
+// https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/),
+// in all cases respecting the provided min and max.
+func exponentialBackoff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
+       if attemptNum > 0 && min < minExponentialBackoffBase {
+               min = minExponentialBackoffBase
+       }
+       var t time.Duration
+       if resp != nil && (resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode == http.StatusServiceUnavailable) {
+               if s := resp.Header.Get("Retry-After"); s != "" {
+                       if sleep, err := strconv.ParseInt(s, 10, 64); err == nil {
+                               t = time.Second * time.Duration(sleep)
+                       } else if stamp, err := time.Parse(time.RFC1123, s); err == nil {
+                               t = stamp.Sub(time.Now())
+                       }
+               }
+       }
+       if t == 0 {
+               jitter := mathrand.New(mathrand.NewSource(int64(time.Now().Nanosecond()))).Float64()
+               t = min + time.Duration((math.Pow(2, float64(attemptNum))*float64(min)-float64(min))*jitter)
+       }
+       if t < min {
+               return min
+       } else if t > max {
+               return max
+       } else {
+               return t
+       }
+}
+
 // DoAndDecode performs req and unmarshals the response (which must be
 // JSON) into dst. Use this instead of RequestAndDecode if you need
 // more control of the http.Request object.
@@ -535,6 +540,12 @@ func (c *Client) RequestAndDecodeContext(ctx context.Context, dst interface{}, m
        if err != nil {
                return err
        }
+       if dst == nil {
+               if urlValues == nil {
+                       urlValues = url.Values{}
+               }
+               urlValues["select"] = []string{`["uuid"]`}
+       }
        if urlValues == nil {
                // Nothing to send
        } else if body != nil || ((method == "GET" || method == "HEAD") && len(urlValues.Encode()) < 1000) {
@@ -619,6 +630,7 @@ type DiscoveryDocument struct {
        GitURL                       string              `json:"gitUrl"`
        Schemas                      map[string]Schema   `json:"schemas"`
        Resources                    map[string]Resource `json:"resources"`
+       Revision                     string              `json:"revision"`
 }
 
 type Resource struct {