This commit is contained in:
Maycon Santos
2025-10-10 00:53:37 +02:00
parent 2288664fe7
commit e67829d1d7
4 changed files with 311 additions and 0 deletions

View File

@@ -17,6 +17,56 @@ Load testing tool for the NetBird signal server.
## Usage
### Standalone Binary
Build and run the load test as a standalone binary:
```bash
# Build the binary
cd signal/loadtest/cmd/signal-loadtest
go build -o signal-loadtest
# Single message exchange
./signal-loadtest \
-server http://localhost:10000 \
-pairs-per-sec 10 \
-total-pairs 100 \
-message-size 100
# Continuous exchange for 30 seconds
./signal-loadtest \
-server http://localhost:10000 \
-pairs-per-sec 10 \
-total-pairs 20 \
-message-size 200 \
-exchange-duration 30s \
-message-interval 200ms
# Long-running test (10 minutes)
./signal-loadtest \
-server http://localhost:10000 \
-pairs-per-sec 20 \
-total-pairs 50 \
-message-size 500 \
-exchange-duration 10m \
-message-interval 100ms \
-test-duration 15m \
-log-level debug
# Show help
./signal-loadtest -h
```
**Available Flags:**
- `-server`: Signal server URL (default: `http://localhost:10000`)
- `-pairs-per-sec`: Peer pairs created per second (default: 10)
- `-total-pairs`: Total number of peer pairs (default: 100)
- `-message-size`: Message size in bytes (default: 100)
- `-test-duration`: Maximum test duration, 0 = unlimited (default: 0)
- `-exchange-duration`: Continuous exchange duration per pair, 0 = single message (default: 0)
- `-message-interval`: Interval between messages in continuous mode (default: 100ms)
- `-log-level`: Log level: trace, debug, info, warn, error (default: info)
### Running Tests
```bash

View File

@@ -0,0 +1,128 @@
package main
import (
"context"
"fmt"
"net"
"os"
"os/exec"
"testing"
"time"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"google.golang.org/grpc"
"github.com/netbirdio/netbird/shared/signal/proto"
"github.com/netbirdio/netbird/signal/server"
)
func TestCLI_SingleMessage(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
grpcServer, serverAddr := startTestSignalServer(t, ctx)
defer grpcServer.Stop()
cmd := exec.Command("go", "run", "main.go",
"-server", serverAddr,
"-pairs-per-sec", "3",
"-total-pairs", "5",
"-message-size", "50",
"-log-level", "warn")
output, err := cmd.CombinedOutput()
require.NoError(t, err, "CLI should execute successfully")
outputStr := string(output)
require.Contains(t, outputStr, "Load Test Report")
require.Contains(t, outputStr, "Total Pairs Sent: 5")
require.Contains(t, outputStr, "Successful Exchanges: 5")
t.Logf("Output:\n%s", outputStr)
}
func TestCLI_ContinuousExchange(t *testing.T) {
if testing.Short() {
t.Skip("Skipping continuous exchange CLI test in short mode")
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
grpcServer, serverAddr := startTestSignalServer(t, ctx)
defer grpcServer.Stop()
cmd := exec.Command("go", "run", "main.go",
"-server", serverAddr,
"-pairs-per-sec", "2",
"-total-pairs", "3",
"-message-size", "100",
"-exchange-duration", "3s",
"-message-interval", "100ms",
"-log-level", "warn")
output, err := cmd.CombinedOutput()
require.NoError(t, err, "CLI should execute successfully")
outputStr := string(output)
require.Contains(t, outputStr, "Load Test Report")
require.Contains(t, outputStr, "Total Pairs Sent: 3")
require.Contains(t, outputStr, "Successful Exchanges: 3")
t.Logf("Output:\n%s", outputStr)
}
func TestCLI_InvalidConfig(t *testing.T) {
tests := []struct {
name string
args []string
}{
{
name: "negative pairs",
args: []string{"-pairs-per-sec", "-1"},
},
{
name: "zero total pairs",
args: []string{"-total-pairs", "0"},
},
{
name: "negative message size",
args: []string{"-message-size", "-100"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
args := append([]string{"run", "main.go"}, tt.args...)
cmd := exec.Command("go", args...)
output, err := cmd.CombinedOutput()
require.Error(t, err, "Should fail with invalid config")
require.Contains(t, string(output), "Configuration error")
})
}
}
func startTestSignalServer(t *testing.T, ctx context.Context) (*grpc.Server, string) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
grpcServer := grpc.NewServer()
signalServer, err := server.NewServer(ctx, otel.Meter("cli-test"))
require.NoError(t, err)
proto.RegisterSignalExchangeServer(grpcServer, signalServer)
go func() {
if err := grpcServer.Serve(listener); err != nil {
t.Logf("Server stopped: %v", err)
}
}()
time.Sleep(100 * time.Millisecond)
return grpcServer, fmt.Sprintf("http://%s", listener.Addr().String())
}
func TestMain(m *testing.M) {
os.Exit(m.Run())
}

View File

@@ -0,0 +1,105 @@
package main
import (
"flag"
"fmt"
"os"
"time"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/signal/loadtest"
)
var (
serverURL string
pairsPerSecond int
totalPairs int
messageSize int
testDuration time.Duration
exchangeDuration time.Duration
messageInterval time.Duration
logLevel string
)
func init() {
flag.StringVar(&serverURL, "server", "http://localhost:10000", "Signal server URL")
flag.IntVar(&pairsPerSecond, "pairs-per-sec", 10, "Number of peer pairs to create per second")
flag.IntVar(&totalPairs, "total-pairs", 100, "Total number of peer pairs to create")
flag.IntVar(&messageSize, "message-size", 100, "Size of test message in bytes")
flag.DurationVar(&testDuration, "test-duration", 0, "Maximum test duration (0 = unlimited)")
flag.DurationVar(&exchangeDuration, "exchange-duration", 0, "Duration for continuous message exchange per pair (0 = single message)")
flag.DurationVar(&messageInterval, "message-interval", 100*time.Millisecond, "Interval between messages in continuous mode")
flag.StringVar(&logLevel, "log-level", "info", "Log level (trace, debug, info, warn, error)")
}
func main() {
flag.Parse()
level, err := log.ParseLevel(logLevel)
if err != nil {
fmt.Fprintf(os.Stderr, "Invalid log level: %v\n", err)
os.Exit(1)
}
log.SetLevel(level)
config := loadtest.LoadTestConfig{
ServerURL: serverURL,
PairsPerSecond: pairsPerSecond,
TotalPairs: totalPairs,
MessageSize: messageSize,
TestDuration: testDuration,
ExchangeDuration: exchangeDuration,
MessageInterval: messageInterval,
}
if err := validateConfig(config); err != nil {
fmt.Fprintf(os.Stderr, "Configuration error: %v\n", err)
flag.Usage()
os.Exit(1)
}
log.Infof("Signal Load Test Configuration:")
log.Infof(" Server URL: %s", config.ServerURL)
log.Infof(" Pairs per second: %d", config.PairsPerSecond)
log.Infof(" Total pairs: %d", config.TotalPairs)
log.Infof(" Message size: %d bytes", config.MessageSize)
if config.TestDuration > 0 {
log.Infof(" Test duration: %v", config.TestDuration)
}
if config.ExchangeDuration > 0 {
log.Infof(" Exchange duration: %v", config.ExchangeDuration)
log.Infof(" Message interval: %v", config.MessageInterval)
} else {
log.Infof(" Mode: Single message exchange")
}
fmt.Println()
lt := loadtest.NewLoadTest(config)
if err := lt.Run(); err != nil {
log.Errorf("Load test failed: %v", err)
os.Exit(1)
}
metrics := lt.GetMetrics()
metrics.PrintReport()
}
func validateConfig(config loadtest.LoadTestConfig) error {
if config.ServerURL == "" {
return fmt.Errorf("server URL is required")
}
if config.PairsPerSecond <= 0 {
return fmt.Errorf("pairs-per-sec must be greater than 0")
}
if config.TotalPairs <= 0 {
return fmt.Errorf("total-pairs must be greater than 0")
}
if config.MessageSize <= 0 {
return fmt.Errorf("message-size must be greater than 0")
}
if config.MessageInterval <= 0 {
return fmt.Errorf("message-interval must be greater than 0")
}
return nil
}

View File

@@ -0,0 +1,28 @@
#!/bin/bash
set -e
echo "Building signal-loadtest binary..."
go build -o signal-loadtest
echo ""
echo "=== Test 1: Single message exchange (5 pairs) ==="
./signal-loadtest \
-server http://localhost:10000 \
-pairs-per-sec 5 \
-total-pairs 5 \
-message-size 50 \
-log-level info
echo ""
echo "=== Test 2: Continuous exchange (3 pairs, 5 seconds) ==="
./signal-loadtest \
-server http://localhost:10000 \
-pairs-per-sec 3 \
-total-pairs 3 \
-message-size 100 \
-exchange-duration 5s \
-message-interval 200ms \
-log-level info
echo ""
echo "All tests completed!"