Repository: vmware/govmomi
Pull request #1758 (2019-12-19 08:40:49)

Add retry support for HTTP status codes
- simulator.StatusSDK can be used to respond with any status code

- soap.Client returns a url.Error instead of errors.New for HTTP status codes

- Custom error type implements the Temporary interface for use by retry handlers

- Add vim25.TemporaryNetworkError example


Patch:
diff --git a/simulator/simulator.go b/simulator/simulator.go
index 944fa987..88108036 100644
--- a/simulator/simulator.go
+++ b/simulator/simulator.go
@@ -412,6 +412,11 @@ func (s *Service) RegisterSDK(r *Registry) {
 	s.ServeMux.HandleFunc(r.Path, s.ServeSDK)
 }
 
+// StatusSDK can be used to simulate an /sdk HTTP response code other than 200.
+// The value of StatusSDK is restored to http.StatusOK after 1 response.
+// This can be useful to test vim25.Retry() for example.
+var StatusSDK = http.StatusOK
+
 // ServeSDK implements the http.Handler interface
 func (s *Service) ServeSDK(w http.ResponseWriter, r *http.Request) {
 	if r.Method != http.MethodPost {
@@ -419,6 +424,12 @@ func (s *Service) ServeSDK(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	if StatusSDK != http.StatusOK {
+		w.WriteHeader(StatusSDK)
+		StatusSDK = http.StatusOK // reset
+		return
+	}
+
 	body, err := s.readAll(r.Body)
 	_ = r.Body.Close()
 	if err != nil {
diff --git a/vim25/retry.go b/vim25/retry.go
index b8807eef..f10e2cb6 100644
--- a/vim25/retry.go
+++ b/vim25/retry.go
@@ -18,8 +18,6 @@ package vim25
 
 import (
 	"context"
-	"net"
-	"net/url"
 	"time"
 
 	"github.com/vmware/govmomi/vim25/soap"
@@ -32,22 +30,18 @@ type RetryFunc func(err error) (retry bool, delay time.Duration)
 // network error (for example: a connect timeout).
 func TemporaryNetworkError(n int) RetryFunc {
 	return func(err error) (retry bool, delay time.Duration) {
-		var nerr net.Error
 		var ok bool
 
-		// Never retry if this is not a network error.
-		switch rerr := err.(type) {
-		case *url.Error:
-			if nerr, ok = rerr.Err.(net.Error); !ok {
-				return false, 0
-			}
-		case net.Error:
-			nerr = rerr
-		default:
+		t, ok := err.(interface {
+			// Temporary is implemented by url.Error and net.Error
+			Temporary() bool
+		})
+		if !ok {
+			// Never retry if this is not a Temporary error.
 			return false, 0
 		}
 
-		if !nerr.Temporary() {
+		if !t.Temporary() {
 			return false, 0
 		}
 
diff --git a/vim25/soap/client.go b/vim25/soap/client.go
index 5b725f33..e6acd3d0 100644
--- a/vim25/soap/client.go
+++ b/vim25/soap/client.go
@@ -532,6 +532,32 @@ func (c *Client) WithHeader(ctx context.Context, header Header) context.Context
 	return context.WithValue(ctx, headerContext{}, header)
 }
 
+type statusError struct {
+	res *http.Response
+}
+
+// Temporary returns true for HTTP response codes that can be retried
+// See vim25.TemporaryNetworkError
+func (e *statusError) Temporary() bool {
+	switch e.res.StatusCode {
+	case http.StatusBadGateway:
+		return true
+	}
+	return false
+}
+
+func (e *statusError) Error() string {
+	return e.res.Status
+}
+
+func newStatusError(res *http.Response) error {
+	return &url.Error{
+		Op:  res.Request.Method,
+		URL: res.Request.URL.Path,
+		Err: &statusError{res},
+	}
+}
+
 func (c *Client) RoundTrip(ctx context.Context, reqBody, resBody HasFault) error {
 	var err error
 	var b []byte
@@ -587,7 +613,7 @@ func (c *Client) RoundTrip(ctx context.Context, reqBody, resBody HasFault) error
 		case http.StatusInternalServerError:
 			// Error, but typically includes a body explaining the error
 		default:
-			return errors.New(res.Status)
+			return newStatusError(res)
 		}
 
 		dec := xml.NewDecoder(res.Body)


Test patch:
diff --git a/vim25/examples_test.go b/vim25/examples_test.go
new file mode 100644
index 00000000..f4bd6999
--- /dev/null
+++ b/vim25/examples_test.go
@@ -0,0 +1,52 @@
+/*
+Copyright (c) 2019 VMware, Inc. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package vim25_test
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+
+	"github.com/vmware/govmomi/find"
+	"github.com/vmware/govmomi/simulator"
+	"github.com/vmware/govmomi/vim25"
+)
+
+func ExampleTemporaryNetworkError() {
+	simulator.Run(func(ctx context.Context, c *vim25.Client) error {
+		// Configure retry handler
+		c.RoundTripper = vim25.Retry(c.Client, vim25.TemporaryNetworkError(3))
+
+		vm, err := find.NewFinder(c).VirtualMachine(ctx, "DC0_H0_VM0")
+		if err != nil {
+			return err
+		}
+
+		// Tell vcsim to respond with 502 on the 1st request
+		simulator.StatusSDK = http.StatusBadGateway
+
+		state, err := vm.PowerState(ctx)
+		if err != nil {
+			return err
+		}
+
+		fmt.Println(state)
+
+		return nil
+	})
+	// Output: poweredOn
+}