Repository: istio/fortio (go)

Issue:
add a warm up mode or a "until some code" mode
for readiness checks it'd be nice to do a fortio run that stops on some code or time

ie . qps=1, c=1, stop_on=200, abort_on=503, ok_codes=-1,404, t=120s

would try 1/sec up to 120s and stop as soon as a 200 or 503 is found and continue if -1 or 404

cc @ayj 

Pull request description:
New "AbortOn" http runner option
Fixes #194

New "AbortOn" http runner option to stop a run when a given code is encountered

Useful for 2 uses cases:

Readiness: Stop when 200 are received

Error/bug finding: Stop when a 503 is received (and not further flood
the logs)

Patch:
diff --git a/README.md b/README.md
index 4d04378..5dab36d 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@ docker run istio/fortio load http://www.google.com/ # For a test run
 Or download the binary distribution, for instance:
 
 ```shell
-curl -L https://github.com/istio/fortio/releases/download/v0.8.1/fortio-linux_x64-0.8.1.tgz \
+curl -L https://github.com/istio/fortio/releases/download/v0.8.2/fortio-linux_x64-0.8.2.tgz \
  | sudo tar -C / -xvzpf -
 ```
 
@@ -41,7 +41,7 @@ You can get a preview of the reporting/graphing UI at https://fortio.istio.io/
 Fortio can be an http or grpc load generator, gathering statistics using the `load` subcommand, or start simple http and grpc ping servers, as well as a basic web UI, result graphing and https redirector, with the `server` command or issue grpc ping messages using the `grpcping` command. It can also fetch a single URL's for debugging when using the `curl` command (or the `-curl` flag to the load command). You can run just the redirector with `redirect`. Lastly if you saved JSON results (using the web UI or directly from the command line), you can browse and graph those results using the `report` command.
 <!-- use release/updateFlags.sh to update this section -->
 ```
-Φορτίο 0.8.1 usage:
+Φορτίο 0.8.2 usage:
 	fortio command [flags] target
 where command is one of: load (load testing), server (starts grpc ping and http
 echo/ui/redirect/proxy servers), grpcping (grpc client), report (report only UI
@@ -54,6 +54,9 @@ target is a url (http load tests) or host:port (grpc health test).  flags are:
 	Proxies to run, e.g -P "localport1 dest_host1:dest_port1"
 	-P "[::1]:0 www.google.com:443" ...
   -a	Automatically save JSON result with filename based on labels & timestamp
+  -abort-on int
+	Http code that if encountered aborts the run. e.g. 503 or -1 for socket
+	errors.
   -allow-initial-errors
 	Allow and don't abort on initial warmup errors
   -base-url string
@@ -156,12 +159,12 @@ target is a url (http load tests) or host:port (grpc health test).  flags are:
 * Start the internal servers:
 ```
 $ fortio server &
-Fortio 0.8.1 grpc 'ping' server listening on [::]:8079
-Fortio 0.8.1 https redirector server listening on [::]:8081
-Fortio 0.8.1 echo server listening on [::]:8080
+Fortio 0.8.2 grpc 'ping' server listening on [::]:8079
+Fortio 0.8.2 https redirector server listening on [::]:8081
+Fortio 0.8.2 echo server listening on [::]:8080
 UI started - visit:
 http://localhost:8080/fortio/   (or any host/ip reachable on this server)
-21:45:23 I fortio_main.go:195> All fortio 0.8.1 buildinfo go1.10 servers started!
+21:45:23 I fortio_main.go:195> All fortio 0.8.2 buildinfo go1.10 servers started!
 ```
 
 * By default, Fortio's web/echo servers listen on port 8080 on all interfaces.
@@ -171,8 +174,8 @@ $ fortio server -http-port 10.10.10.10:8088
 UI starting - visit:
 http://10.10.10.10:8088/fortio/
 Https redirector running on :8081
-Fortio 0.8.1 grpc ping server listening on port :8079
-Fortio 0.8.1 echo server listening on port 10.10.10.10:8088
+Fortio 0.8.2 grpc ping server listening on port :8079
+Fortio 0.8.2 echo server listening on port 10.10.10.10:8088
 ```
 * Simple grpc ping:
 ```
@@ -242,14 +245,14 @@ Content-Type: text/plain; charset=UTF-8
 Date: Mon, 08 Jan 2018 22:26:26 GMT
 Content-Length: 230
 
-Φορτίο version 0.8.1 echo debug server up for 39s on ldemailly-macbookpro - request from [::1]:65055
+Φορτίο version 0.8.2 echo debug server up for 39s on ldemailly-macbookpro - request from [::1]:65055
 
 GET /debug HTTP/1.1
 
 headers:
 
 Host: localhost:8080
-User-Agent: istio/fortio-0.8.1
+User-Agent: istio/fortio-0.8.2
 Foo: Bar
 
 body:
diff --git a/fhttp/httprunner.go b/fhttp/httprunner.go
index e083197..4b7f2c3 100644
--- a/fhttp/httprunner.go
+++ b/fhttp/httprunner.go
@@ -44,6 +44,9 @@ type HTTPRunnerResults struct {
 	HeaderSizes *stats.HistogramData
 	URL         string
 	SocketCount int
+	// http code to abort the run on (-1 for connection or other socket error)
+	AbortOn int
+	aborter *periodic.Aborter
 }
 
 // Run tests http request fetching. Main call being run at the target QPS.
@@ -52,10 +55,14 @@ func (httpstate *HTTPRunnerResults) Run(t int) {
 	log.Debugf("Calling in %d", t)
 	code, body, headerSize := httpstate.client.Fetch()
 	size := len(body)
-	log.Debugf("Got in %3d hsz %d sz %d", code, headerSize, size)
+	log.Debugf("Got in %3d hsz %d sz %d - will abort on %d", code, headerSize, size, httpstate.AbortOn)
 	httpstate.RetCodes[code]++
 	httpstate.sizes.Record(float64(size))
 	httpstate.headerSizes.Record(float64(headerSize))
+	if httpstate.AbortOn == code {
+		log.Infof("Aborting run because of code %d", code)
+		httpstate.aborter.Abort()
+	}
 }
 
 // HTTPRunnerOptions includes the base RunnerOptions plus http specific
@@ -65,6 +72,8 @@ type HTTPRunnerOptions struct {
 	HTTPOptions               // Need to call Init() to initialize
 	Profiler           string // file to save profiles to. defaults to no profiling
 	AllowInitialErrors bool   // whether initial errors don't cause an abort
+	// Which status code cause an abort of the run (default 0 = don't abort; reminder -1 is returned for socket errors)
+	AbortOn int
 }
 
 // RunHTTPTest runs an http test and returns the aggregated stats.
@@ -81,6 +90,8 @@ func RunHTTPTest(o *HTTPRunnerOptions) (*HTTPRunnerResults, error) {
 		sizes:       stats.NewHistogram(0, 100),
 		headerSizes: stats.NewHistogram(0, 5),
 		URL:         o.URL,
+		AbortOn:     o.AbortOn,
+		aborter:     r.Options().Stop,
 	}
 	httpstate := make([]HTTPRunnerResults, numThreads)
 	for i := 0; i < numThreads; i++ {
@@ -103,6 +114,8 @@ func RunHTTPTest(o *HTTPRunnerOptions) (*HTTPRunnerResults, error) {
 		httpstate[i].sizes = total.sizes.Clone()
 		httpstate[i].headerSizes = total.headerSizes.Clone()
 		httpstate[i].RetCodes = make(map[int]int64)
+		httpstate[i].AbortOn = total.AbortOn
+		httpstate[i].aborter = total.aborter
 	}
 
 	if o.Profiler != "" {
diff --git a/fortio_main.go b/fortio_main.go
index 3e059a5..7ce0383 100644
--- a/fortio_main.go
+++ b/fortio_main.go
@@ -99,6 +99,7 @@ var (
 	defaultDataDir = "."
 
 	allowInitialErrorsFlag = flag.Bool("allow-initial-errors", false, "Allow and don't abort on initial warmup errors")
+	abortOnFlag            = flag.Int("abort-on", 0, "Http code that if encountered aborts the run. e.g. 503 or -1 for socket errors.")
 	autoSaveFlag           = flag.Bool("a", false, "Automatically save JSON result with filename based on labels & timestamp")
 	redirectFlag           = flag.String("redirect-port", "8081", "Redirect all incoming traffic to https URL"+
 		" (need ingress to work properly). Can be in the form of host:port, ip:port, port or \"disabled\" to disable the feature.")
@@ -260,6 +261,7 @@ func fortioLoad(justCurl bool, percList []float64) {
 			RunnerOptions:      ro,
 			Profiler:           *profileFlag,
 			AllowInitialErrors: *allowInitialErrorsFlag,
+			AbortOn:            *abortOnFlag,
 		}
 		res, err = fhttp.RunHTTPTest(&o)
 	}


Test patch:
diff --git a/fhttp/httprunner_test.go b/fhttp/httprunner_test.go
index 75e6b4a..685ea76 100644
--- a/fhttp/httprunner_test.go
+++ b/fhttp/httprunner_test.go
@@ -225,3 +225,41 @@ func TestServe(t *testing.T) {
 		t.Errorf("Multi header not found in %s", DebugSummary(data, 1024))
 	}
 }
+
+func TestAbortOn(t *testing.T) {
+	mux, addr := DynamicHTTPServer(false)
+	mux.HandleFunc("/foo/", EchoHandler)
+	baseURL := fmt.Sprintf("http://localhost:%d/", addr.Port)
+	o := HTTPRunnerOptions{}
+	o.URL = baseURL
+	o.AbortOn = 404
+	o.Exactly = 40
+	o.NumThreads = 4
+	o.QPS = 10
+	r, err := RunHTTPTest(&o)
+	if err != nil {
+		t.Errorf("Error while starting runner1: %v", err)
+	}
+	count := r.Result().DurationHistogram.Count
+	if count > int64(o.NumThreads) {
+		t.Errorf("Abort1 not working, did %d requests expecting ideally 1 and <= %d", count, o.NumThreads)
+	}
+	o.URL += "foo/"
+	r, err = RunHTTPTest(&o)
+	if err != nil {
+		t.Errorf("Error while starting runner2: %v", err)
+	}
+	count = r.Result().DurationHistogram.Count
+	if count != o.Exactly {
+		t.Errorf("Did %d requests when expecting all %d (non matching AbortOn)", count, o.Exactly)
+	}
+	o.AbortOn = 200
+	r, err = RunHTTPTest(&o)
+	if err != nil {
+		t.Errorf("Error while starting runner3: %v", err)
+	}
+	count = r.Result().DurationHistogram.Count
+	if count > int64(o.NumThreads) {
+		t.Errorf("Abort2 not working, did %d requests expecting ideally 1 and <= %d", count, o.NumThreads)
+	}
+}