diff --git a/cmd/config.go b/cmd/config.go index 22e87d1..a0fb4fd 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -23,18 +23,19 @@ func init() { } type RunConfig struct { - Target string - ParsedTarget *url.URL - Requests int - Concurrency int - Timeout time.Duration - Duration time.Duration - Method string - Body string - BodyFile string - Headers []string - Verbose bool - DisableRedirects bool + Target string + ParsedTarget *url.URL + Requests int + Concurrency int + Timeout time.Duration + Duration time.Duration + Method string + Body string + BodyFile string + Headers []string + Verbose bool + DisableRedirects bool + DisableKeepAlives bool } var validMethods = map[string]bool{ @@ -100,15 +101,16 @@ func (c *RunConfig) Validate() error { func (c *RunConfig) ToHTTPConfig() httpclient.Config { return httpclient.Config{ - Target: c.Target, - Requests: c.Requests, - Concurrency: c.Concurrency, - Timeout: c.Timeout, - Duration: c.Duration, - Method: c.Method, - Body: c.Body, - Headers: c.Headers, - Verbose: c.Verbose, - DisableRedirects: c.DisableRedirects, + Target: c.Target, + Requests: c.Requests, + Concurrency: c.Concurrency, + Timeout: c.Timeout, + Duration: c.Duration, + Method: c.Method, + Body: c.Body, + Headers: c.Headers, + Verbose: c.Verbose, + DisableRedirects: c.DisableRedirects, + DisableKeepAlives: c.DisableKeepAlives, } } diff --git a/cmd/configfile.go b/cmd/configfile.go index a6e247c..2fca8d8 100644 --- a/cmd/configfile.go +++ b/cmd/configfile.go @@ -13,17 +13,18 @@ import ( ) type fileConfig struct { - Target *string `json:"target" yaml:"target"` - Requests *int `json:"requests" yaml:"requests"` - Concurrency *int `json:"concurrency" yaml:"concurrency"` - Timeout *string `json:"timeout" yaml:"timeout"` - Duration *string `json:"duration" yaml:"duration"` - Method *string `json:"method" yaml:"method"` - Body *string `json:"body" yaml:"body"` - BodyFile *string `json:"body_file" yaml:"body_file"` - Headers []string `json:"headers" yaml:"headers"` - Verbose *bool `json:"verbose" yaml:"verbose"` - DisableRedirects *bool `json:"disable_redirects" yaml:"disable_redirects"` + Target *string `json:"target" yaml:"target"` + Requests *int `json:"requests" yaml:"requests"` + Concurrency *int `json:"concurrency" yaml:"concurrency"` + Timeout *string `json:"timeout" yaml:"timeout"` + Duration *string `json:"duration" yaml:"duration"` + Method *string `json:"method" yaml:"method"` + Body *string `json:"body" yaml:"body"` + BodyFile *string `json:"body_file" yaml:"body_file"` + Headers []string `json:"headers" yaml:"headers"` + Verbose *bool `json:"verbose" yaml:"verbose"` + DisableRedirects *bool `json:"disable_redirects" yaml:"disable_redirects"` + DisableKeepAlives *bool `json:"disable_keepalive" yaml:"disable_keepalive"` } func loadConfig(path string) (*fileConfig, error) { @@ -135,5 +136,9 @@ func mergeConfig(file *fileConfig, cli RunConfig, changed map[string]bool) (RunC merged.DisableRedirects = *file.DisableRedirects } + if file.DisableKeepAlives != nil && !changed["disable-keepalive"] { + merged.DisableKeepAlives = *file.DisableKeepAlives + } + return merged, nil } diff --git a/cmd/run.go b/cmd/run.go index 3888532..36fc991 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -58,6 +58,7 @@ Latency Percentiles: headers, _ := f.GetStringArray("header") verbose, _ := f.GetBool("verbose") disableRedirects, _ := f.GetBool("disable-redirects") + disableKeepAlives, _ := f.GetBool("disable-keepalive") outputFormat, _ := f.GetString("output") if outputFormat != "text" && outputFormat != "json" { @@ -70,17 +71,18 @@ Latency Percentiles: } cliConfig := RunConfig{ - Target: target, - Requests: requests, - Concurrency: concurrency, - Timeout: timeout, - Duration: duration, - Method: strings.ToUpper(method), - Body: body, - BodyFile: bodyFile, - Headers: headers, - Verbose: verbose, - DisableRedirects: disableRedirects, + Target: target, + Requests: requests, + Concurrency: concurrency, + Timeout: timeout, + Duration: duration, + Method: strings.ToUpper(method), + Body: body, + BodyFile: bodyFile, + Headers: headers, + Verbose: verbose, + DisableRedirects: disableRedirects, + DisableKeepAlives: disableKeepAlives, } changed := make(map[string]bool) @@ -145,6 +147,7 @@ Latency Percentiles: cmd.Flags().StringP("config", "f", "", "Path to configuration file (JSON/YAML)") cmd.Flags().BoolP("verbose", "v", false, "Enable verbose output") cmd.Flags().Bool("disable-redirects", false, "Do not follow HTTP redirects") + cmd.Flags().Bool("disable-keepalive", false, "Disable HTTP keep-alive, forcing a new connection per request") cmd.Flags().StringP("output", "o", "text", "Output format (text or json)") return cmd diff --git a/cmd/run_test.go b/cmd/run_test.go index e1c4bf2..f16441e 100644 --- a/cmd/run_test.go +++ b/cmd/run_test.go @@ -28,6 +28,7 @@ func TestFlagRegistration(t *testing.T) { {"config", "f", ""}, {"verbose", "v", false}, {"disable-redirects", "", false}, + {"disable-keepalive", "", false}, {"output", "o", "text"}, } diff --git a/internal/httpclient/client.go b/internal/httpclient/client.go index eb635f3..c6be0ea 100644 --- a/internal/httpclient/client.go +++ b/internal/httpclient/client.go @@ -16,11 +16,12 @@ import ( "github.com/infraspecdev/goperf/internal/stats" ) -func NewHTTPClient(concurrency int, disableRedirects bool) *http.Client { +func NewHTTPClient(concurrency int, disableRedirects bool, disableKeepAlives bool) *http.Client { client := &http.Client{ Transport: &http.Transport{ MaxIdleConnsPerHost: concurrency, DisableCompression: true, + DisableKeepAlives: disableKeepAlives, }, } if disableRedirects { @@ -32,18 +33,19 @@ func NewHTTPClient(concurrency int, disableRedirects bool) *http.Client { } type Config struct { - Target string - Requests int - Concurrency int - Timeout time.Duration - Duration time.Duration - Method string - Body string - Headers []string - Verbose bool - Version string - Stderr io.Writer - DisableRedirects bool + Target string + Requests int + Concurrency int + Timeout time.Duration + Duration time.Duration + Method string + Body string + Headers []string + Verbose bool + Version string + Stderr io.Writer + DisableRedirects bool + DisableKeepAlives bool } type HTTPDoer interface { @@ -157,7 +159,7 @@ func recordResult(ctx context.Context, recorder *stats.HistogramRecorder, verbos } func Run(ctx context.Context, cfg Config) *stats.HistogramRecorder { - client := NewHTTPClient(cfg.Concurrency, cfg.DisableRedirects) + client := NewHTTPClient(cfg.Concurrency, cfg.DisableRedirects, cfg.DisableKeepAlives) recorder := stats.NewHistogramRecorder(cfg.Timeout) var verboseWriter io.Writer diff --git a/internal/httpclient/client_test.go b/internal/httpclient/client_test.go index e10317a..4970f42 100644 --- a/internal/httpclient/client_test.go +++ b/internal/httpclient/client_test.go @@ -18,7 +18,7 @@ import ( const testTimeout = 2 * time.Second func TestNewHTTPClient(t *testing.T) { - client := NewHTTPClient(50, false) + client := NewHTTPClient(50, false, true) tr, ok := client.Transport.(*http.Transport) if !ok { @@ -30,6 +30,9 @@ func TestNewHTTPClient(t *testing.T) { if !tr.DisableCompression { t.Error("expected DisableCompression=true") } + if !tr.DisableKeepAlives { + t.Error("expected DisableKeepAlives=true") + } } func TestMakeRequestSuccess(t *testing.T) { @@ -811,7 +814,7 @@ func TestNewHTTPClient_Redirects(t *testing.T) { })) defer server.Close() - client := NewHTTPClient(1, tt.disableRedirects) + client := NewHTTPClient(1, tt.disableRedirects, false) cfg := Config{ Target: server.URL, Timeout: testTimeout,