mirror of
				https://github.com/XRPLF/clio.git
				synced 2025-11-04 11:55: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