fiddlecli - Break out fiddle retries into its own library.

Bug: skia:8582
Change-Id: I91d009c97dd796e06028c55f56674a139ddcf42e
Reviewed-on: https://skia-review.googlesource.com/c/177909
Reviewed-by: Ravi Mistry <rmistry@google.com>
Commit-Queue: Ravi Mistry <rmistry@google.com>
Auto-Submit: Joe Gregorio <jcgregorio@google.com>
diff --git a/fiddlek/go/client/client.go b/fiddlek/go/client/client.go
new file mode 100644
index 0000000..00bf42a
--- /dev/null
+++ b/fiddlek/go/client/client.go
@@ -0,0 +1,75 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"time"
+
+	"go.skia.org/infra/fiddlek/go/types"
+	"go.skia.org/infra/go/httputils"
+	"go.skia.org/infra/go/sklog"
+	"go.skia.org/infra/go/util"
+)
+
+const (
+	RETRIES = 10
+)
+
+// singleRequest does a single request to run a fiddle
+//
+// c - HTTP client.
+// body - The body to send to the fiddle /_/run endpoint.
+// domain - The scheme and domain name to make requests to, e.g. "https://fiddle.skia.org".
+// sleep - The duration to sleep if the request fails.
+// failFast - If true then fail fatally.
+func singleRequest(c *http.Client, body []byte, domain string, sleep time.Duration, failFast bool) (*types.RunResults, bool) {
+	resp, err := c.Post(domain+"/_/run", "application/json", bytes.NewReader(body))
+	if err != nil {
+		sklog.Infof("Send error: %s", err)
+		time.Sleep(sleep)
+		return nil, false
+	}
+	defer util.Close(resp.Body)
+	if resp.StatusCode != 200 {
+		if failFast {
+			sklog.Fatalf("Send failed, with fail_fast set: %s", resp.Status)
+		}
+		sklog.Infof("Send failed: %s", resp.Status)
+		time.Sleep(sleep)
+		return nil, false
+	}
+	var runResults types.RunResults
+	if err := json.NewDecoder(resp.Body).Decode(&runResults); err != nil {
+		sklog.Infof("Malformed response: %s", err)
+		time.Sleep(sleep)
+		return nil, false
+	}
+	return &runResults, true
+}
+
+// Do runs a single fiddle contained in 'body'.
+//
+// failFast - If true then fail fatally.
+// domain - The scheme and domain name to make requests to, e.g. "https://fiddle.skia.org".
+// validator - A function that does extra validation on the fiddle run results. Return true if the
+//   response is valid.
+func Do(body []byte, failFast bool, domain string, validator func(*types.RunResults) bool) (*types.RunResults, bool) {
+	c := httputils.NewTimeoutClient()
+	success := false
+	sleep := time.Second
+	var runResults *types.RunResults
+	for tries := 0; tries < RETRIES; tries++ {
+		// POST to fiddle.
+		runResults, success = singleRequest(c, body, domain, sleep, failFast)
+		if success {
+			if validator(runResults) {
+				return runResults, success
+			}
+		}
+		sleep *= 2
+		fmt.Print("x")
+	}
+	return nil, false
+}
diff --git a/fiddlek/go/fiddlecli/main.go b/fiddlek/go/fiddlecli/main.go
index 1c6a600..b25d0b7 100644
--- a/fiddlek/go/fiddlecli/main.go
+++ b/fiddlek/go/fiddlecli/main.go
@@ -5,30 +5,24 @@
 package main
 
 import (
-	"bytes"
 	"encoding/json"
 	"flag"
 	"fmt"
 	"io/ioutil"
 	"log"
-	"net/http"
 	"os"
 	"sync"
-	"time"
 
+	"go.skia.org/infra/fiddlek/go/client"
 	"go.skia.org/infra/fiddlek/go/types"
 	"go.skia.org/infra/go/common"
-	"go.skia.org/infra/go/httputils"
 	"go.skia.org/infra/go/sklog"
-	"go.skia.org/infra/go/util"
 	"golang.org/x/sync/errgroup"
 )
 
 const (
-	RETRIES = 10
-
 	// VERSION of the application. Update for major and minor changes to functionality.
-	VERSION = "1.0"
+	VERSION = "1.1"
 )
 
 // flags
@@ -49,33 +43,6 @@
 	req *types.FiddleContext
 }
 
-// singleRequest does a single request to fiddle.skia.org.
-func singleRequest(c *http.Client, body []byte, domain string) (*types.RunResults, bool) {
-	resp, err := c.Post(domain+"/_/run", "application/json", bytes.NewReader(body))
-	sleep := time.Second
-	if err != nil {
-		sklog.Infof("Send error: %s", err)
-		time.Sleep(sleep)
-		return nil, false
-	}
-	defer util.Close(resp.Body)
-	if resp.StatusCode != 200 {
-		if *failFast {
-			sklog.Fatalf("Send failed, with fail_fast set: %s", resp.Status)
-		}
-		sklog.Infof("Send failed: %s", resp.Status)
-		time.Sleep(sleep)
-		return nil, false
-	}
-	var runResults types.RunResults
-	if err := json.NewDecoder(resp.Body).Decode(&runResults); err != nil {
-		sklog.Infof("Malformed response: %s", err)
-		time.Sleep(sleep)
-		return nil, false
-	}
-	return &runResults, true
-}
-
 func main() {
 	// Check flags.
 	common.Init()
@@ -134,30 +101,20 @@
 					mutex.Unlock()
 					continue
 				}
-				success := false
-				var runResults *types.RunResults
-				for tries := 0; tries < RETRIES; tries++ {
-					c := httputils.NewTimeoutClient()
-					// POST to fiddle.
-					b, err = json.Marshal(req.req)
-					if err != nil {
-						sklog.Errorf("Failed to encode an individual request: %s", err)
-						break
-					}
-					runResults, success = singleRequest(c, b, *domain)
-					if success {
-						if fiddleHash != runResults.FiddleHash {
-							sklog.Warningf("Got mismatched hashes for %s: Want %q != Got %q", req.id, fiddleHash, runResults.FiddleHash)
-						} else {
-							break
-						}
-					} else {
-						sklog.Warningf("Send failed for: %s", req.id)
-					}
-					fmt.Print("x")
+				b, err = json.Marshal(req.req)
+				if err != nil {
+					sklog.Errorf("Failed to encode an individual request: %s", err)
+					continue
 				}
+				runResults, success := client.Do(b, *failFast, *domain, func(runResults *types.RunResults) bool {
+					if fiddleHash != runResults.FiddleHash {
+						sklog.Warningf("Got mismatched hashes for %s: Want %q != Got %q", req.id, fiddleHash, runResults.FiddleHash)
+						return false
+					}
+					return true
+				})
 				if !success {
-					sklog.Errorf("Failed to make request after %d tries", RETRIES)
+					sklog.Errorf("Failed to make request after retries")
 					continue
 				}
 
diff --git a/named-fiddles/go/named-fiddles/main.go b/named-fiddles/go/named-fiddles/main.go
index ae1271a..c8505a3 100644
--- a/named-fiddles/go/named-fiddles/main.go
+++ b/named-fiddles/go/named-fiddles/main.go
@@ -16,6 +16,7 @@
 	"github.com/gorilla/csrf"
 	"github.com/gorilla/mux"
 	"github.com/unrolled/secure"
+	"go.skia.org/infra/fiddlek/go/client"
 	"go.skia.org/infra/fiddlek/go/store"
 	"go.skia.org/infra/fiddlek/go/types"
 	"go.skia.org/infra/go/allowed"
@@ -41,11 +42,6 @@
 	resourcesDir       = flag.String("resources_dir", "", "The directory to find templates, JS, and CSS files. If blank the current directory will be used.")
 )
 
-const (
-	// NUM_RETRIES is the number of time to try to run a fiddle before giving up.
-	NUM_RETRIES = 5
-)
-
 // Server is the state of the server.
 type Server struct {
 	store     *store.Store
@@ -103,26 +99,15 @@
 	}
 
 	// Then re-run it.
-	var resp *http.Response
-	for i := 0; i < NUM_RETRIES; i++ {
-		resp, err = c.Post("https://fiddle.skia.org/_/run", "application/json", getResp.Body)
-		if err != nil || resp.StatusCode != 200 {
-			status := ""
-			if resp != nil {
-				status = resp.Status
-			}
-			sklog.Errorf("Failed to run %q: %s %s", n.Name, status, err)
-			continue
-		}
-		break
-	}
-	if err != nil || resp.StatusCode != 200 {
-		srv.errorsInRun.Inc(1)
+	b, err := ioutil.ReadAll(getResp.Body)
+	if err != nil {
+		sklog.Warningf("Failed to read fiddle: %s", err)
 		return true
 	}
-	var runResults types.RunResults
-	if err := json.NewDecoder(resp.Body).Decode(&runResults); err != nil {
-		sklog.Errorf("Failed to decode run results: %s", err)
+	runResults, success := client.Do(b, false, "https://fiddle.skia.org", func(*types.RunResults) bool {
+		return true
+	})
+	if success {
 		srv.errorsInRun.Inc(1)
 		return true
 	}