-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhammer.go
146 lines (117 loc) · 3.32 KB
/
hammer.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
package main
import (
"context"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"strings"
"runtime"
"time"
"golang.org/x/net/http2"
"golang.org/x/sync/semaphore"
)
type hammerSettings struct {
concurrencyFactor int
client *http.Client
delay time.Duration
sem *semaphore.Weighted
requestHeaders http.Header
urls []string
useHTTP2 bool
}
func getSettings() (*hammerSettings, error) {
settings := hammerSettings{
requestHeaders: make(http.Header),
}
// set flags
flag.IntVar(&settings.concurrencyFactor, "c", runtime.GOMAXPROCS(0), "Concurrency factor, number of requests to make concurrently. Defaults to the value of runtime.GOMAXPROCS(0)")
flag.DurationVar(&settings.delay, "d", 0, "Delay to wait after making a request")
flag.BoolVar(&settings.useHTTP2, "http2", false, "Use HTTP/2 for requests")
flag.Func("header", "Set a request header in the form \"header-name: header-value\" (multiple invocations allowed)", func(str string) error {
h := strings.SplitN(str, ":", 2)
if len(h) != 2 {
return fmt.Errorf("Header must be in the form \"header-name: header-value\"")
}
settings.requestHeaders.Add(h[0], h[1])
return nil
})
// parse flags
flag.Parse()
// post process flags
settings.urls = flag.Args()
if len(settings.urls) == 0 {
return nil, errors.New("must specify at least one url")
}
settings.sem = semaphore.NewWeighted(int64(settings.concurrencyFactor))
settings.client = &http.Client{}
if settings.useHTTP2 {
settings.client.Transport = &http2.Transport{AllowHTTP: true}
}
return &settings, nil
}
func usage() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options] <url> [urls...]\n", os.Args[0])
flag.PrintDefaults()
}
func logStats(n int, startTime time.Time) {
t := time.Now()
elapsed := t.Sub(startTime)
avgRate := float64(n) / float64(elapsed/time.Millisecond) * 1000.0 * 60.0
fmt.Printf("\rCompleted %d total requests in %v (average %0.2f requests/minute)...", n, elapsed.Round(1000*time.Millisecond), avgRate)
}
// Make a request against a url
func doRequest(settings *hammerSettings, i int) {
defer settings.sem.Release(1)
url := settings.urls[i%len(settings.urls)]
req, err := http.NewRequest("GET", url, nil)
if err != nil {
fmt.Println(err)
return
}
req.Close = false
req.Header = settings.requestHeaders.Clone()
res, err := settings.client.Do(req)
if err != nil {
fmt.Println(err)
return
}
if res.StatusCode != 200 && res.StatusCode != 404 {
fmt.Println("\nReceived non 200 response:", res.StatusCode, url)
}
// read the response body
go func() {
io.Copy(ioutil.Discard, res.Body)
res.Body.Close()
}()
if settings.delay != 0 {
time.Sleep(settings.delay)
}
}
func loadTest(settings *hammerSettings) {
fmt.Println("max concurrent requests:", settings.concurrencyFactor)
fmt.Println("max parallel goroutines:", runtime.GOMAXPROCS(0))
ctx := context.Background()
start := time.Now()
for i := 0; true; i++ {
if err := settings.sem.Acquire(ctx, 1); err != nil {
fmt.Printf("Failed to acquire semaphore: %v", err)
break
}
go doRequest(settings, i)
go logStats(i, start)
}
}
func main() {
flag.Usage = usage
settings, err := getSettings()
if err != nil {
fmt.Fprintf(os.Stderr, "Incorrect usage: %v\n\n", err)
flag.Usage()
return
}
loadTest(settings)
}