Skip to content

Commit 670792f

Browse files
butonicrhafer
authored andcommitted
benchmark client enhancements
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
1 parent 2b083d8 commit 670792f

1 file changed

Lines changed: 132 additions & 14 deletions

File tree

opencloud/pkg/command/benchmark.go

Lines changed: 132 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package command
22

33
import (
44
"bytes"
5+
"context"
56
"crypto/tls"
67
"encoding/base64"
78
"errors"
@@ -10,8 +11,10 @@ import (
1011
"net/http"
1112
"os"
1213
"os/exec"
14+
"os/signal"
1315
"strconv"
1416
"strings"
17+
"syscall"
1518
"time"
1619

1720
"github.com/olekukonko/tablewriter"
@@ -38,26 +41,97 @@ func BenchmarkCommand(cfg *config.Config) *cobra.Command {
3841
func BenchmarkClientCommand(cfg *config.Config) *cobra.Command {
3942
benchClientCmd := &cobra.Command{
4043
Use: "client",
41-
Short: "Start a client that continuously makes web requests and prints stats. The options mimic curl, but URL must be at the end.",
44+
Short: "Start a client that continuously makes web requests and prints stats. The options mimic curl, but we default to PROPFIND requests.",
4245
RunE: func(cmd *cobra.Command, args []string) error {
4346
jobs, err := cmd.Flags().GetInt("jobs")
4447
if err != nil {
4548
return err
4649
}
4750
insecure, _ := cmd.Flags().GetBool("insecure")
4851
opt := clientOptions{
49-
request: cmd.Flag("request").Value.String(),
5052
url: args[0],
5153
insecure: insecure,
5254
jobs: jobs,
5355
headers: make(map[string]string),
54-
data: []byte(cmd.Flag("data").Value.String()),
5556
}
57+
58+
if d, _ := cmd.Flags().GetString("data-raw"); d != "" {
59+
opt.request = "POST"
60+
opt.headers["Content-Type"] = "application/x-www-form-urlencoded"
61+
opt.data = []byte(d)
62+
}
63+
64+
if d, _ := cmd.Flags().GetString("data"); d != "" {
65+
opt.request = "POST"
66+
opt.headers["Content-Type"] = "application/x-www-form-urlencoded"
67+
if strings.HasPrefix(d, "@") {
68+
filePath := strings.TrimPrefix(d, "@")
69+
var data []byte
70+
var err error
71+
72+
// read from file or stdin and trim trailing newlines
73+
if filePath == "-" {
74+
data, err = os.ReadFile("/dev/stdin")
75+
} else {
76+
data, err = os.ReadFile(filePath)
77+
}
78+
if err != nil {
79+
log.Fatal(errors.New("could not read data from file '" + filePath + "': " + err.Error()))
80+
}
81+
82+
// clean byte array similar to curl's --data parameter
83+
// It removes leading/trailing whitespace and converts line breaks to spaces
84+
85+
// Trim leading and trailing whitespace
86+
data = bytes.TrimSpace(data)
87+
88+
// Replace newlines and carriage returns with spaces
89+
data = bytes.ReplaceAll(data, []byte("\r\n"), []byte(" "))
90+
data = bytes.ReplaceAll(data, []byte("\n"), []byte(" "))
91+
data = bytes.ReplaceAll(data, []byte("\r"), []byte(" "))
92+
93+
// Replace multiple spaces with single space
94+
for bytes.Contains(data, []byte(" ")) {
95+
data = bytes.ReplaceAll(data, []byte(" "), []byte(" "))
96+
}
97+
98+
opt.data = data
99+
} else {
100+
opt.data = []byte(d)
101+
}
102+
}
103+
104+
if d, _ := cmd.Flags().GetString("data-binary"); d != "" {
105+
opt.request = "POST"
106+
opt.headers["Content-Type"] = "application/x-www-form-urlencoded"
107+
if strings.HasPrefix(d, "@") {
108+
filePath := strings.TrimPrefix(d, "@")
109+
var data []byte
110+
var err error
111+
if filePath == "-" {
112+
data, err = os.ReadFile("/dev/stdin")
113+
} else {
114+
data, err = os.ReadFile(filePath)
115+
}
116+
if err != nil {
117+
log.Fatal(errors.New("could not read data from file '" + filePath + "': " + err.Error()))
118+
}
119+
opt.data = data
120+
} else {
121+
opt.data = []byte(d)
122+
}
123+
}
124+
125+
// override method if specified
126+
if request, _ := cmd.Flags().GetString("request"); request != "" {
127+
opt.request = request
128+
}
129+
56130
if opt.url == "" {
57131
log.Fatal(errors.New("no URL specified"))
58132
}
59133

60-
headersSlice, err := cmd.Flags().GetStringSlice("headers")
134+
headersSlice, err := cmd.Flags().GetStringSlice("header")
61135
if err != nil {
62136
return err
63137
}
@@ -124,17 +198,29 @@ func BenchmarkClientCommand(cfg *config.Config) *cobra.Command {
124198
defer opt.ticker.Stop()
125199
}
126200

127-
return client(opt)
201+
// Set up signal handling for Ctrl+C
202+
ctx, cancel := context.WithCancel(cmd.Context())
203+
defer cancel()
204+
205+
sigChan := make(chan os.Signal, 1)
206+
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
207+
go func() {
208+
<-sigChan
209+
fmt.Println("\nReceived interrupt signal, shutting down...")
210+
cancel()
211+
}()
212+
return client(ctx, opt)
128213

129214
},
130215
}
131216

132-
// TODO with v3 'flag.Persistent: true' can be set to make the order of flags no longer relevant \o/
133217
// flags mimicing curl
134218
benchClientCmd.Flags().StringP("request", "X", "PROPFIND", "Specifies a custom request method to use when communicating with the HTTP server.")
135219
benchClientCmd.Flags().StringP("user", "u", "admin:admin", "Specify the user name and password to use for server authentication.")
136220
benchClientCmd.Flags().BoolP("insecure", "k", false, "Skip the TLS verification step and proceed without checking.")
137-
benchClientCmd.Flags().StringP("data", "d", "", "Sends the specified data in a request to the HTTP server.")
221+
benchClientCmd.Flags().StringP("data", "d", "", "Sends the specified data in a POST request to the HTTP server, in the same way that a browser does when a user has filled in an HTML form and presses the submit button. If you start the data with the letter @, the rest should be a file name to read the data from, or - if you want to read the data from stdin. When -d, --data is told to read from a file like that, carriage returns and newlines are stripped out. If you do not want the @ character to have a special interpretation use --data-raw instead.")
222+
benchClientCmd.Flags().StringP("data-raw", "", "", "Sends the specified data in a request to the HTTP server.")
223+
benchClientCmd.Flags().StringP("data-binary", "", "", "This posts data exactly as specified with no extra processing whatsoever. If you start the data with the letter @, the rest should be a file name to read the data from, or - if you want to read the data from stdin.")
138224
benchClientCmd.Flags().StringSliceP("headers", "H", []string{}, "Extra header to include in information sent.")
139225
benchClientCmd.Flags().String("rate", "", "Specify the maximum transfer frequency you allow a client to use - in number of transfer starts per time unit (sometimes called request rate). The request rate is provided as \"N/U\" where N is an integer number and U is a time unit. Supported units are 's' (second), 'm' (minute), 'h' (hour) and 'd' /(day, as in a 24 hour unit). The default time unit, if no \"/U\" is provided, is number of transfers per hour.")
140226

@@ -158,8 +244,7 @@ type clientOptions struct {
158244
jobs int
159245
}
160246

161-
func client(o clientOptions) error {
162-
247+
func client(ctx context.Context, o clientOptions) error {
163248
type stat struct {
164249
job int
165250
duration time.Duration
@@ -178,6 +263,13 @@ func client(o clientOptions) error {
178263

179264
cookies := map[string]*http.Cookie{}
180265
for {
266+
// Check if context is cancelled
267+
select {
268+
case <-ctx.Done():
269+
return
270+
default:
271+
}
272+
181273
req, err := http.NewRequest(o.request, o.url, bytes.NewReader(o.data))
182274
if err != nil {
183275
log.Printf("client %d: could not create request: %s\n", i, err)
@@ -195,20 +287,35 @@ func client(o clientOptions) error {
195287
res, err := client.Do(req)
196288
duration := -time.Until(start)
197289
if err != nil {
290+
// Check if error is due to context cancellation
291+
if ctx.Err() != nil {
292+
return
293+
}
198294
log.Printf("client %d: could not create request: %s\n", i, err)
199295
time.Sleep(time.Second)
200296
} else {
201297
res.Body.Close()
202-
stats <- stat{
298+
select {
299+
case stats <- stat{
203300
job: i,
204301
duration: duration,
205302
status: res.StatusCode,
303+
}:
304+
case <-ctx.Done():
305+
return
206306
}
207307
for _, c := range res.Cookies() {
208308
cookies[c.Name] = c
209309
}
210310
}
211-
time.Sleep(o.rateDelay - duration)
311+
// Sleep with context awareness
312+
if o.rateDelay > duration {
313+
select {
314+
case <-time.After(o.rateDelay - duration):
315+
case <-ctx.Done():
316+
return
317+
}
318+
}
212319
}
213320
}(i)
214321
}
@@ -217,9 +324,14 @@ func client(o clientOptions) error {
217324
if o.ticker == nil {
218325
// no ticker, just write every request
219326
for {
220-
stat := <-stats
221-
numRequests++
222-
fmt.Printf("req %d took %v and returned status %d\n", numRequests, stat.duration, stat.status)
327+
select {
328+
case stat := <-stats:
329+
numRequests++
330+
fmt.Printf("req %d took %v and returned status %d\n", numRequests, stat.duration, stat.status)
331+
case <-ctx.Done():
332+
fmt.Println("\nShutting down...")
333+
return nil
334+
}
223335
}
224336
}
225337

@@ -235,6 +347,12 @@ func client(o clientOptions) error {
235347
numRequests = 0
236348
duration = 0
237349
}
350+
case <-ctx.Done():
351+
if numRequests > 0 {
352+
fmt.Printf("\n%d req at %v/req\n", numRequests, duration/time.Duration(numRequests))
353+
}
354+
fmt.Println("Shutting down...")
355+
return nil
238356
}
239357
}
240358

0 commit comments

Comments
 (0)