Merge branch 'master' into 7490-datamanager-dont-die-return-error
[arvados.git] / sdk / go / arvadosclient / arvadosclient.go
index cc99efdcf4b5deb327113780fdb8bf355163becf..18e1074bf6f6c801c21593bc57586a902807f8b4 100644 (file)
@@ -14,6 +14,7 @@ import (
        "os"
        "regexp"
        "strings"
+       "time"
 )
 
 type StringMatcher func(string) bool
@@ -25,6 +26,12 @@ var MissingArvadosApiHost = errors.New("Missing required environment variable AR
 var MissingArvadosApiToken = errors.New("Missing required environment variable ARVADOS_API_TOKEN")
 var ErrInvalidArgument = errors.New("Invalid argument")
 
+// A common failure mode is to reuse a keepalive connection that has been
+// terminated (in a way that we can't detect) for being idle too long.
+// POST and DELETE are not safe to retry automatically, so we minimize
+// such failures by always using a new or recently active socket.
+var MaxIdleConnectionDuration = 30 * time.Second
+
 // Indicates an error that was returned by the API server.
 type APIServerError struct {
        // Address of server returning error, of the form "host:port".
@@ -76,44 +83,25 @@ type ArvadosClient struct {
 
        // Discovery document
        DiscoveryDoc Dict
-}
 
-// APIConfig struct consists of:
-//    APIToken        string
-//    APIHost         string
-//    APIHostInsecure bool
-//    ExternalClient  bool
-type APIConfig struct {
-       APIToken        string
-       APIHost         string
-       APIHostInsecure bool
-       ExternalClient  bool
+       lastClosedIdlesAt time.Time
 }
 
-// Create a new ArvadosClient, initialized with standard Arvados environment variables
-// ARVADOS_API_HOST, ARVADOS_API_TOKEN, ARVADOS_API_HOST_INSECURE, ARVADOS_EXTERNAL_CLIENT.
+// Create a new ArvadosClient, initialized with standard Arvados environment
+// variables ARVADOS_API_HOST, ARVADOS_API_TOKEN, and (optionally)
+// ARVADOS_API_HOST_INSECURE.
 func MakeArvadosClient() (ac ArvadosClient, err error) {
-       var config APIConfig
-       config.APIToken = os.Getenv("ARVADOS_API_TOKEN")
-       config.APIHost = os.Getenv("ARVADOS_API_HOST")
-
        var matchTrue = regexp.MustCompile("^(?i:1|yes|true)$")
+       insecure := matchTrue.MatchString(os.Getenv("ARVADOS_API_HOST_INSECURE"))
+       external := matchTrue.MatchString(os.Getenv("ARVADOS_EXTERNAL_CLIENT"))
 
-       config.APIHostInsecure = matchTrue.MatchString(os.Getenv("ARVADOS_API_HOST_INSECURE"))
-       config.ExternalClient = matchTrue.MatchString(os.Getenv("ARVADOS_EXTERNAL_CLIENT"))
-
-       return MakeArvadosClientWithConfig(config)
-}
-
-// Create a new ArvadosClient, using the given input parameters.
-func MakeArvadosClientWithConfig(config APIConfig) (ac ArvadosClient, err error) {
        ac = ArvadosClient{
-               ApiServer:   config.APIHost,
-               ApiToken:    config.APIToken,
-               ApiInsecure: config.APIHostInsecure,
+               ApiServer:   os.Getenv("ARVADOS_API_HOST"),
+               ApiToken:    os.Getenv("ARVADOS_API_TOKEN"),
+               ApiInsecure: insecure,
                Client: &http.Client{Transport: &http.Transport{
-                       TLSClientConfig: &tls.Config{InsecureSkipVerify: config.APIHostInsecure}}},
-               External: config.ExternalClient}
+                       TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure}}},
+               External: external}
 
        if ac.ApiServer == "" {
                return ac, MissingArvadosApiHost
@@ -122,6 +110,8 @@ func MakeArvadosClientWithConfig(config APIConfig) (ac ArvadosClient, err error)
                return ac, MissingArvadosApiToken
        }
 
+       ac.lastClosedIdlesAt = time.Now()
+
        return ac, err
 }
 
@@ -179,6 +169,15 @@ func (c ArvadosClient) CallRaw(method string, resourceType string, uuid string,
                req.Header.Add("X-External-Client", "1")
        }
 
+       // POST and DELETE are not safe to retry automatically, so we minimize
+       // such failures by always using a new or recently active socket
+       if method == "POST" || method == "DELETE" {
+               if time.Since(c.lastClosedIdlesAt) > MaxIdleConnectionDuration {
+                       c.lastClosedIdlesAt = time.Now()
+                       c.Client.Transport.(*http.Transport).CloseIdleConnections()
+               }
+       }
+
        // Make the request
        var resp *http.Response
        if resp, err = c.Client.Do(req); err != nil {