Repository: fortio/fortio
Pull request #522 (2022-03-19 19:45:41)

`-server-idle-timeout` and close=x as a % instead of just true/false for all echo requests, `-calc-qps` client side
2 new server features: 

- ability to set  `-server-idle-timeout`
- for the echo server the `close=` param now can take a % to roll whether to close

1 new client/load feature:
- calculate the target qps to get the `-n` requests in `-t` time using `-calc-qps` (convenience function to for instance spread calls across idle timeouts...)

1 cosmetic improvement in end of run output (#517):
```
Sockets used: 0 (for perfect keepalive, would be 1)
Jitter: false
Uniform: true
```
Replaced by
```
Uniform: true, Jitter: true
```
for uniform client (skips the 0 socket output)

codeclimate[bot]: Code Climate has analyzed commit 99b70679 and detected **1 issue** on this pull request.

Here's the issue category breakdown:

| Category | Count |
|:---:|:---:|
| Complexity | 1 |


View more on [Code Climate](https://codeclimate.com/github/fortio/fortio/pull/522).

codecov[bot]: # [Codecov](https://codecov.io/gh/fortio/fortio/pull/522?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=fortio) Report
> Merging [#522](https://codecov.io/gh/fortio/fortio/pull/522?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=fortio) (1af8d14) into [master](https://codecov.io/gh/fortio/fortio/commit/f82e822e2dec925e7a7c85368971560b80bfcdec?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=fortio) (f82e822) will **increase** coverage by `0.2%`.
> The diff coverage is `100.0%`.

```diff
@@           Coverage Diff            @@
##           master    #522     +/-   ##
========================================
+ Coverage    86.0%   86.1%   +0.2%     
========================================
  Files          26      26             
  Lines        3476    3492     +16     
========================================
+ Hits         2988    3008     +20     
+ Misses        337     333      -4     
  Partials      151     151             
```


| [Impacted Files](https://codecov.io/gh/fortio/fortio/pull/522?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=fortio) | Coverage Δ | |
|---|---|---|
| [fhttp/http\_server.go](https://codecov.io/gh/fortio/fortio/pull/522/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=fortio#diff-Zmh0dHAvaHR0cF9zZXJ2ZXIuZ28=) | `80.8% <100.0%> (+0.1%)` | :arrow_up: |
| [fhttp/http\_utils.go](https://codecov.io/gh/fortio/fortio/pull/522/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=fortio#diff-Zmh0dHAvaHR0cF91dGlscy5nbw==) | `100.0% <100.0%> (ø)` | |
| [fhttp/http\_client.go](https://codecov.io/gh/fortio/fortio/pull/522/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=fortio#diff-Zmh0dHAvaHR0cF9jbGllbnQuZ28=) | `80.0% <0.0%> (+0.8%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/fortio/fortio/pull/522?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=fortio).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=fortio)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/fortio/fortio/pull/522?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=fortio). Last update [f82e822...1af8d14](https://codecov.io/gh/fortio/fortio/pull/522?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=fortio). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=fortio).


Patch:
diff --git a/README.md b/README.md
index f839948..85e98fe 100644
--- a/README.md
+++ b/README.md
@@ -92,6 +92,8 @@ Most important flags for http load generation:
 | `-c connections` | Number of parallel simultaneous connections (and matching go routine) |
 | `-t duration` | How long to run the test  (for instance `-t 30m` for 30 minutes) or 0 to run until ^C, example (default 5s) |
 | `-n numcalls` | Run for exactly this number of calls instead of duration. Default (0) is to use duration (-t). |
+| `-payload str` or `-payload-file fname` | Switch to using POST with the given payload (see also `-payload-size` for random payload)|
+| `-uniform` | Spread the calls across threads |
 | `-r resolution` | Resolution of the histogram lowest buckets in seconds (default 0.001 i.e 1ms), use 1/10th of your expected typical latency |
 | `-H "header: value"` | Can be specified multiple times to add headers (including Host:) |
 | `-a`     |  Automatically save JSON result with filename based on labels and timestamp |
@@ -140,6 +142,8 @@ url from the first request is used)
   -cacert Path
         Path to a custom CA certificate file to be used for the TLS client
 connections, if empty, use https:// prefix for standard internet/system CAs
+  -calc-qps
+        Calculate the qps based on number of requests (-n) and duration (-t)
   -cert Path
         Path to the certificate file to be used for client or server TLS
   -compression
@@ -269,6 +273,8 @@ server mode
   -sequential-warmup
         http(s) runner warmup done in parallel instead of sequentially. When
 set, restores pre 1.21 behavior
+  -server-idle-timeout value
+        Default IdleTimeout for servers (default 30s)
   -static-dir path
         Deprecated/unused path.
   -stdclient
@@ -319,7 +325,7 @@ Fortio `server` has the following feature for the http listening on 8080 (all pa
 | delay     | duration to delay the response by. Can be a single value or a comma separated list of probabilities, e.g `delay=150us:10,2ms:5,0.5s:1` for 10% of chance of a 150 us delay, 5% of a 2ms delay and 1% of a 1/2 second delay |
 | status    | http status to return instead of 200. Can be a single value or a comma separated list of probabilities, e.g `status=404:10,503:5,429:1` for 10% of chance of a 404 status, 5% of a 503 status and 1% of a 429 status |
 | size      | size of the payload to reply instead of echoing input. Also works as probabilities list. `size=1024:10,512:5` 10% of response will be 1k and 5% will be 512 bytes payload and the rest defaults to echoing back. |
-| close     | close the socket after answering e.g `close=true` |
+| close     | close the socket after answering e.g `close=true` to close after all requests or `close=5.3` to close after approximately 5.3% of requests|
 | header    | header(s) to add to the reply e.g. `&header=Foo:Bar&header=X:Y` |
 
 You can set a default value for all these by passing `-echo-server-default-params` to the server command line, for instance:
diff --git a/fhttp/http_server.go b/fhttp/http_server.go
index 01dc559..f6e9c3e 100644
--- a/fhttp/http_server.go
+++ b/fhttp/http_server.go
@@ -51,6 +51,7 @@ var (
 		"Default parameters/querystring to use if there isn't one provided explicitly. E.g \"status=404&delay=3s\"")
 	fetch2CopiesAllHeader = dflag.DynBool(flag.CommandLine, "proxy-all-headers", true,
 		"Determines if only tracing or all headers (and cookies) are copied from request on the fetch2 ui/server endpoint")
+	serverIdleTimeout = dflag.DynDuration(flag.CommandLine, "server-idle-timeout", 30*time.Second, "Default IdleTimeout for servers")
 )
 
 // EchoHandler is an http server handler echoing back the input.
@@ -97,7 +98,7 @@ func EchoHandler(w http.ResponseWriter, r *http.Request) {
 		rqNum := atomic.AddInt64(&EchoRequests, 1)
 		log.Debugf("Request # %v", rqNum)
 	}
-	if r.FormValue("close") != "" {
+	if generateClose(r.FormValue("close")) {
 		log.Debugf("Adding Connection:close / will close socket")
 		w.Header().Set("Connection", "close")
 	}
@@ -167,7 +168,8 @@ func HTTPServer(name string, port string) (*http.ServeMux, net.Addr) {
 	m := http.NewServeMux()
 	h2s := &http2.Server{}
 	s := &http.Server{
-		Handler: h2c.NewHandler(m, h2s),
+		IdleTimeout: serverIdleTimeout.Get(),
+		Handler:     h2c.NewHandler(m, h2s),
 	}
 	listener, addr := fnet.Listen(name, port)
 	if listener == nil {
diff --git a/fhttp/http_utils.go b/fhttp/http_utils.go
index 46f65d1..6f2d1da 100644
--- a/fhttp/http_utils.go
+++ b/fhttp/http_utils.go
@@ -398,6 +398,28 @@ func generateDelay(delay string) time.Duration {
 	return 0
 }
 
+// generateClose from string, format: close=true for 100% close
+// close=true:10 or close=10 for 10% socket close.
+func generateClose(closeStr string) bool {
+	if closeStr == "" || closeStr == "false" {
+		return false
+	}
+	if closeStr == "true" { // avoid throwing error for pre 1.22 syntax
+		return true
+	}
+	p, err := strconv.ParseFloat(closeStr, 32)
+	if err != nil {
+		log.Debugf("error %v parsing close=%q treating as true", err, closeStr)
+		return true
+	}
+	res := 100. * rand.Float32() // nolint: gosec // we want fast not crypto
+	log.Debugf("close=%f rolled %f", p, res)
+	if res <= float32(p) {
+		return true
+	}
+	return false
+}
+
 // RoundDuration rounds to 10th of second.
 func RoundDuration(d time.Duration) time.Duration {
 	return d.Round(100 * time.Millisecond)
diff --git a/fhttp/httprunner.go b/fhttp/httprunner.go
index f3873b4..b29d0bb 100644
--- a/fhttp/httprunner.go
+++ b/fhttp/httprunner.go
@@ -77,7 +77,7 @@ type HTTPRunnerOptions struct {
 }
 
 // RunHTTPTest runs an http test and returns the aggregated stats.
-// nolint: funlen, gocognit
+// nolint: funlen, gocognit, gocyclo
 func RunHTTPTest(o *HTTPRunnerOptions) (*HTTPRunnerResults, error) {
 	o.RunType = "HTTP"
 	warmupMode := "parallel"
@@ -192,9 +192,10 @@ func RunHTTPTest(o *HTTPRunnerOptions) (*HTTPRunnerResults, error) {
 	r.Options().ReleaseRunners()
 	sort.Ints(keys)
 	totalCount := float64(total.DurationHistogram.Count)
-	_, _ = fmt.Fprintf(out, "Sockets used: %d (for perfect keepalive, would be %d)\n", total.SocketCount, r.Options().NumThreads)
-	_, _ = fmt.Fprintf(out, "Jitter: %t\n", total.Jitter)
-	_, _ = fmt.Fprintf(out, "Uniform: %t\n", total.Uniform)
+	if !o.DisableFastClient {
+		_, _ = fmt.Fprintf(out, "Sockets used: %d (for perfect keepalive, would be %d)\n", total.SocketCount, r.Options().NumThreads)
+	}
+	_, _ = fmt.Fprintf(out, "Uniform: %t, Jitter: %t\n", total.Uniform, total.Jitter)
 	for _, k := range keys {
 		_, _ = fmt.Fprintf(out, "Code %3d : %d (%.1f %%)\n", k, total.RetCodes[k], 100.*float64(total.RetCodes[k])/totalCount)
 	}
diff --git a/fortio_main.go b/fortio_main.go
index 270275a..6cbef74 100644
--- a/fortio_main.go
+++ b/fortio_main.go
@@ -180,6 +180,7 @@ var (
 		"file `path` to log all requests to. Maybe have performance impacts")
 	accessLogFileFormat = flag.String("access-log-format", "json",
 		"`format` for access log. Supported values: [json, influx]")
+	calcQPS = flag.Bool("calc-qps", false, "Calculate the qps based on number of requests (-n) and duration (-t)")
 )
 
 // nolint: funlen // well yes it's fairly big and lotsa ifs.
@@ -345,7 +346,7 @@ func fortioNC() {
 	}
 }
 
-// nolint: funlen // maybe refactor/shorten later.
+// nolint: funlen, gocognit // maybe refactor/shorten later.
 func fortioLoad(justCurl bool, percList []float64) {
 	if len(flag.Args()) != 1 {
 		usageErr("Error: fortio load/curl needs a url or destination")
@@ -359,6 +360,13 @@ func fortioLoad(justCurl bool, percList []float64) {
 	prevGoMaxProcs := runtime.GOMAXPROCS(*goMaxProcsFlag)
 	out := os.Stderr
 	qps := *qpsFlag // TODO possibly use translated <=0 to "max" from results/options normalization in periodic/
+	if *calcQPS {
+		if *exactlyFlag == 0 || *durationFlag <= 0 {
+			usageErr("Error: can't use `-calc-qps` without also specifying `-n` and `-t`")
+		}
+		qps = float64(*exactlyFlag) / (*durationFlag).Seconds()
+		log.LogVf("Calculated QPS to do %d request in %v: %f", *exactlyFlag, *durationFlag, qps)
+	}
 	_, _ = fmt.Fprintf(out, "Fortio %s running at %g queries per second, %d->%d procs",
 		version.Short(), qps, prevGoMaxProcs, runtime.GOMAXPROCS(0))
 	if *exactlyFlag > 0 {


Test patch:
diff --git a/fhttp/http_test.go b/fhttp/http_test.go
index ef1f59e..8aa4017 100644
--- a/fhttp/http_test.go
+++ b/fhttp/http_test.go
@@ -497,6 +497,28 @@ func TestGenerateSize(t *testing.T) {
 	}
 }
 
+func TestGenerateClose(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected bool
+	}{
+		// not numbers
+		{"true", true},
+		{"false", false},
+		{"x", true},
+		// Numbers
+		{"0", false},
+		{"0.0", false},
+		{"99.9999999", true}, // well, in theory this should fail once in a blue moon
+		{"100", true},
+	}
+	for _, tst := range tests {
+		if actual := generateClose(tst.input); actual != tst.expected {
+			t.Errorf("Got %v, expected %v for generateClose(%q)", actual, tst.expected, tst.input)
+		}
+	}
+}
+
 func TestPayloadWithEchoBack(t *testing.T) {
 	tests := []struct {
 		payload           []byte
diff --git a/fhttp/httprunner_test.go b/fhttp/httprunner_test.go
index ddf5965..3c42c2f 100644
--- a/fhttp/httprunner_test.go
+++ b/fhttp/httprunner_test.go
@@ -187,7 +187,7 @@ func TestHTTPRunnerClientRace(t *testing.T) {
 func TestClosingAndSocketCount(t *testing.T) {
 	mux, addr := DynamicHTTPServer(false)
 	mux.HandleFunc("/echo42/", EchoHandler)
-	URL := fmt.Sprintf("http://localhost:%d/echo42/?close=1", addr.Port)
+	URL := fmt.Sprintf("http://localhost:%d/echo42/?close=true", addr.Port)
 	opts := HTTPRunnerOptions{}
 	opts.Init(URL)
 	opts.QPS = 10