diff --git a/CHANGELOG.md b/CHANGELOG.md index c05b2100..4d146b6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - [PR #367](https://github.com/rqlite/rqlite/pull/367): Remove superflous leading space at CLI prompt. - [PR #368](https://github.com/rqlite/rqlite/pull/368): CLI displays clear error message when not authorized. - [PR #370](https://github.com/rqlite/rqlite/pull/370): CLI does not need to indent JSON when making requests. +- [PR #373](https://github.com/rqlite/rqlite/pull/373): Add simple INSERT-only benchmarking tool. ## 4.2.0 (October 19th 2017) - [PR #354](https://github.com/rqlite/rqlite/pull/354): Vendor Raft. diff --git a/cmd/rqbench/http.go b/cmd/rqbench/http.go new file mode 100644 index 00000000..a3c7d22c --- /dev/null +++ b/cmd/rqbench/http.go @@ -0,0 +1,60 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +type HTTPTester struct { + client http.Client + url string + br *bytes.Reader +} + +func NewHTTPTester(addr string) *HTTPTester { + return &HTTPTester{ + client: http.Client{}, + url: fmt.Sprintf("http://%s:/db/execute", addr), + } +} + +func (h *HTTPTester) Prepare(stmt string, bSz int, tx bool) error { + s := make([]string, bSz) + for i := 0; i < len(s); i++ { + s[i] = stmt + } + + b, err := json.Marshal(s) + if err != nil { + return err + } + h.br = bytes.NewReader(b) + + if tx { + h.url = h.url + "?transaction" + } + + return nil +} + +func (h *HTTPTester) Once() (time.Duration, error) { + h.br.Seek(0, io.SeekStart) + + start := time.Now() + resp, err := h.client.Post(h.url, "application/json", h.br) + if err != nil { + return 0, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return 0, fmt.Errorf("received %s", resp.Status) + } + dur := time.Since(start) + + return dur, nil +} diff --git a/cmd/rqbench/main.go b/cmd/rqbench/main.go new file mode 100644 index 00000000..203b3c00 --- /dev/null +++ b/cmd/rqbench/main.go @@ -0,0 +1,83 @@ +/* +Command rqbench is a rqlite load test utility. +*/ + +package main + +import ( + "flag" + "fmt" + "time" + + "os" +) + +var addr string +var numReqs int +var batchSz int +var tx bool +var tp string + +const name = `rqbench` +const desc = `rqbench is a simple load testing utility for rqlite.` + +func init() { + flag.StringVar(&addr, "a", "localhost:4001", "Node address") + flag.IntVar(&numReqs, "n", 100, "Number of requests") + flag.IntVar(&batchSz, "b", 1, "Statements per request") + flag.BoolVar(&tx, "x", false, "Use explicit transaction per request") + flag.StringVar(&tp, "t", "http", "Transport to use") + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "\n%s\n\n", desc) + fmt.Fprintf(os.Stderr, "Usage: %s [arguments] \n", name) + flag.PrintDefaults() + } +} + +func main() { + flag.Parse() + + // Ensure the SQL statement is set + if flag.NArg() == 0 { + flag.Usage() + os.Exit(1) + } + stmt := flag.Args()[0] + + if tp != "http" { + fmt.Fprintf(os.Stderr, "not a valid transport: %s\n", tp) + } + + tester := NewHTTPTester(addr) + if err := tester.Prepare(stmt, batchSz, tx); err != nil { + fmt.Println("failed to prepare test:", err.Error()) + os.Exit(1) + } + + d, err := run(tester, numReqs) + if err != nil { + fmt.Println("failed to run test:", err.Error()) + os.Exit(1) + } + fmt.Println("Total duration:", d) + fmt.Printf("Requests/sec: %.2f\n", float64((numReqs))/d.Seconds()) + fmt.Printf("Statements/sec: %.2f\n", float64((numReqs*batchSz))/d.Seconds()) +} + +type Tester interface { + Prepare(stmt string, bSz int, tx bool) error + Once() (time.Duration, error) +} + +func run(t Tester, n int) (time.Duration, error) { + var dur time.Duration + + for i := 0; i < n; i++ { + if d, err := t.Once(); err != nil { + return 0, err + } else { + dur += d + } + } + return dur, nil +}