mirror of
https://github.com/XRPLF/clio.git
synced 2025-11-04 20:05:51 +00:00
1
tools/requests_gun/.gitignore
vendored
Normal file
1
tools/requests_gun/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
requests_gun
|
||||
10
tools/requests_gun/README.md
Normal file
10
tools/requests_gun/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# 🔫 Requests gun
|
||||
|
||||
Requests gun is a simple tool that allows you to send multiple requests to a server with specific rps (requests per second) rate.
|
||||
This tool is useful for testing the server behaviour under a specific load.
|
||||
It takes a file that contains json request per line and sends them to the server in a loop with the specified rps rate.
|
||||
|
||||
The tool checks http status code of each request and whether the response body contains field 'error' or not.
|
||||
It prints statistics for each second while running.
|
||||
|
||||
Run `requests_gun --help` to see the available options.
|
||||
1
tools/requests_gun/ammo.txt
Normal file
1
tools/requests_gun/ammo.txt
Normal file
@@ -0,0 +1 @@
|
||||
{ "method": "server_definitions", "params": [ {} ] }
|
||||
5
tools/requests_gun/go.mod
Normal file
5
tools/requests_gun/go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module requests_gun
|
||||
|
||||
go 1.22.2
|
||||
|
||||
require github.com/spf13/pflag v1.0.5 // indirect
|
||||
2
tools/requests_gun/go.sum
Normal file
2
tools/requests_gun/go.sum
Normal file
@@ -0,0 +1,2 @@
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
36
tools/requests_gun/internal/ammo_provider/ammo_provider.go
Normal file
36
tools/requests_gun/internal/ammo_provider/ammo_provider.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package ammo_provider
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type AmmoProvider struct {
|
||||
ammo []string
|
||||
currentBullet atomic.Uint64
|
||||
}
|
||||
|
||||
func (ap *AmmoProvider) getIndex() uint64 {
|
||||
if ap.currentBullet.Load() >= uint64(len(ap.ammo)) {
|
||||
ap.currentBullet.Store(1)
|
||||
return 0
|
||||
}
|
||||
result := ap.currentBullet.Load()
|
||||
ap.currentBullet.Add(1)
|
||||
return result
|
||||
}
|
||||
|
||||
func (ap *AmmoProvider) GetBullet() string {
|
||||
return ap.ammo[ap.getIndex()]
|
||||
}
|
||||
|
||||
func New(reader io.Reader) *AmmoProvider {
|
||||
scanner := bufio.NewScanner(reader)
|
||||
var ammo []string
|
||||
for scanner.Scan() {
|
||||
ammo = append(ammo, scanner.Text())
|
||||
}
|
||||
|
||||
return &AmmoProvider{ammo: ammo}
|
||||
}
|
||||
40
tools/requests_gun/internal/parse_args/parse_args.go
Normal file
40
tools/requests_gun/internal/parse_args/parse_args.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package parse_args
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
flag "github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
type CliArgs struct {
|
||||
Url string
|
||||
Port uint
|
||||
TargetLoad uint
|
||||
Ammo string
|
||||
PrintErrors bool
|
||||
Help bool
|
||||
}
|
||||
|
||||
func Parse() (*CliArgs, error) {
|
||||
flag.Usage = PrintUsage
|
||||
url := flag.StringP("url", "u", "localhost", "URL to send the request to")
|
||||
port := flag.UintP("port", "p", 51233, "Port to send the request to")
|
||||
target_load := flag.UintP("load", "l", 100, "Target requests per second load")
|
||||
print_errors := flag.BoolP("print-errors", "e", false, "Print errors")
|
||||
help := flag.BoolP("help", "h", false, "Print help message")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if flag.NArg() == 0 {
|
||||
return nil, fmt.Errorf("No ammo file provided")
|
||||
}
|
||||
|
||||
return &CliArgs{*url, *port, *target_load, flag.Arg(0), *print_errors, *help}, nil
|
||||
}
|
||||
|
||||
func PrintUsage() {
|
||||
fmt.Println("Usage: requests_gun [options] <ammo_file>")
|
||||
fmt.Println(" <ammo_file> Path to file with requests to use")
|
||||
fmt.Println("Options:")
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
74
tools/requests_gun/internal/request_maker/request_maker.go
Normal file
74
tools/requests_gun/internal/request_maker/request_maker.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package request_maker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RequestMaker interface {
|
||||
MakeRequest(request string) (*ResponseData, error)
|
||||
}
|
||||
|
||||
type HttpRequestMaker struct {
|
||||
url string
|
||||
transport *http.Transport
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
type JsonMap map[string]interface{}
|
||||
type StatusCode int
|
||||
|
||||
type ResponseData struct {
|
||||
Body JsonMap
|
||||
StatusCode StatusCode
|
||||
StatusStr string
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
func (h *HttpRequestMaker) MakeRequest(request string) (*ResponseData, error) {
|
||||
startTime := time.Now()
|
||||
req, err := http.NewRequest("POST", h.url, strings.NewReader(request))
|
||||
if err != nil {
|
||||
return nil, errors.New("Error creating request: " + err.Error())
|
||||
}
|
||||
|
||||
response, err := h.client.Do(req)
|
||||
requestDuration := time.Since(startTime)
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.New("Error making request: " + err.Error())
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
if err != nil {
|
||||
return nil, errors.New("Error reading response body: " + err.Error())
|
||||
}
|
||||
|
||||
if response.StatusCode != 200 {
|
||||
return &ResponseData{StatusCode: StatusCode(response.StatusCode), StatusStr: response.Status + ": " + string(body)}, nil
|
||||
}
|
||||
|
||||
var bodyParsed JsonMap
|
||||
err = json.Unmarshal(body, &bodyParsed)
|
||||
if err != nil {
|
||||
return nil, errors.New("Error parsing response '" + string(body) + "': " + err.Error())
|
||||
}
|
||||
|
||||
return &ResponseData{bodyParsed, StatusCode(response.StatusCode), response.Status, requestDuration}, nil
|
||||
}
|
||||
|
||||
func NewHttp(host string, port uint) *HttpRequestMaker {
|
||||
if !strings.HasPrefix(host, "http://") && !strings.HasPrefix(host, "https://") {
|
||||
host = "http://" + host
|
||||
}
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
client := &http.Client{Transport: transport}
|
||||
|
||||
return &HttpRequestMaker{host + ":" + fmt.Sprintf("%d", port), transport, client}
|
||||
}
|
||||
100
tools/requests_gun/internal/trigger/trigger.go
Normal file
100
tools/requests_gun/internal/trigger/trigger.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package trigger
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"requests_gun/internal/ammo_provider"
|
||||
"requests_gun/internal/parse_args"
|
||||
"requests_gun/internal/request_maker"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Fire(ammoProvider *ammo_provider.AmmoProvider, args *parse_args.CliArgs) {
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, os.Interrupt)
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
ticker := time.NewTicker(time.Second)
|
||||
for {
|
||||
select {
|
||||
case s := <-interrupt:
|
||||
log.Println("Got signal:", s)
|
||||
log.Println("Stopping...")
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
statistics := statistics{startTime: time.Now(), printErrors: args.PrintErrors}
|
||||
doShot := func() {
|
||||
defer wg.Done()
|
||||
bullet := ammoProvider.GetBullet()
|
||||
requestMaker := request_maker.NewHttp(args.Url, args.Port)
|
||||
responseData, err := requestMaker.MakeRequest(bullet)
|
||||
statistics.add(responseData, err)
|
||||
}
|
||||
|
||||
secondStart := time.Now()
|
||||
requestsNumber := uint(0)
|
||||
for requestsNumber < args.TargetLoad && time.Since(secondStart) < time.Second {
|
||||
wg.Add(1)
|
||||
go doShot()
|
||||
requestsNumber++
|
||||
}
|
||||
wg.Wait()
|
||||
if time.Since(secondStart) > time.Second {
|
||||
log.Println("Requests took longer than a second, probably need to decrease the load.")
|
||||
}
|
||||
statistics.print()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type counters struct {
|
||||
totalRequests atomic.Uint64
|
||||
errors atomic.Uint64
|
||||
badReply atomic.Uint64
|
||||
goodReply atomic.Uint64
|
||||
}
|
||||
type statistics struct {
|
||||
counters counters
|
||||
startTime time.Time
|
||||
printErrors bool
|
||||
}
|
||||
|
||||
func (s *statistics) add(response *request_maker.ResponseData, err error) {
|
||||
s.counters.totalRequests.Add(1)
|
||||
if err != nil {
|
||||
if s.printErrors {
|
||||
log.Println("Error making request:", err)
|
||||
}
|
||||
s.counters.errors.Add(1)
|
||||
return
|
||||
}
|
||||
if response.StatusCode != 200 || response.Body["error"] != nil {
|
||||
if s.printErrors {
|
||||
log.Print("Response contains error: ", response.StatusStr)
|
||||
if response.Body["error"] != nil {
|
||||
log.Println(" ", response.Body["error"])
|
||||
} else {
|
||||
log.Println()
|
||||
}
|
||||
}
|
||||
s.counters.badReply.Add(1)
|
||||
} else {
|
||||
s.counters.goodReply.Add(1)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *statistics) print() {
|
||||
elapsed := time.Since(s.startTime)
|
||||
if elapsed < time.Second {
|
||||
elapsed = time.Second
|
||||
}
|
||||
log.Printf("Speed: %.1f rps, Errors: %.1f%%, Bad response: %.1f%%, Good response: %.1f%%\n",
|
||||
float64(s.counters.totalRequests.Load())/elapsed.Seconds(),
|
||||
float64(s.counters.errors.Load())/float64(s.counters.totalRequests.Load())*100,
|
||||
float64(s.counters.badReply.Load())/float64(s.counters.totalRequests.Load())*100,
|
||||
float64(s.counters.goodReply.Load())/float64(s.counters.totalRequests.Load())*100)
|
||||
}
|
||||
31
tools/requests_gun/requests_gun.go
Normal file
31
tools/requests_gun/requests_gun.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"requests_gun/internal/ammo_provider"
|
||||
"requests_gun/internal/parse_args"
|
||||
"requests_gun/internal/trigger"
|
||||
)
|
||||
|
||||
func main() {
|
||||
args, err := parse_args.Parse()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error: ", err)
|
||||
parse_args.PrintUsage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Print("Loading ammo... ")
|
||||
f, err := os.Open(args.Ammo)
|
||||
if err != nil {
|
||||
fmt.Println("Error opening file '", args.Ammo, "': ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
ammoProvider := ammo_provider.New(f)
|
||||
f.Close()
|
||||
fmt.Println("Done")
|
||||
|
||||
fmt.Println("Firing requests...")
|
||||
trigger.Fire(ammoProvider, args)
|
||||
}
|
||||
Reference in New Issue
Block a user