Go service that load tests the Cast SMS/OTP API with configurable concurrent threads, target RPS, ramp-up period, and message parts. Features live stats, final report with SMS parts tracking, and config warnings for suboptimal test parameters. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
129 lines
3.4 KiB
Go
129 lines
3.4 KiB
Go
package runner
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"cast-loadtest/internal/config"
|
|
"cast-loadtest/internal/sender"
|
|
"cast-loadtest/internal/stats"
|
|
)
|
|
|
|
func Run(cfg config.Config) {
|
|
fmt.Println("========================================")
|
|
fmt.Println(" SMS API LOAD TESTER (Go) ")
|
|
fmt.Println("========================================")
|
|
fmt.Println()
|
|
fmt.Println("--- API Configuration ---")
|
|
fmt.Printf(" Target: %s\n", cfg.APIURL)
|
|
fmt.Printf(" API Key: %s...%s\n", cfg.APIKey[:9], cfg.APIKey[len(cfg.APIKey)-4:])
|
|
fmt.Printf(" Sender ID: %s\n", cfg.SenderID)
|
|
fmt.Printf(" Recipient: %s\n", cfg.Recipient)
|
|
fmt.Println()
|
|
fmt.Println("--- Message Configuration ---")
|
|
fmt.Printf(" SMS Parts: %d\n", cfg.MsgParts)
|
|
fmt.Printf(" Max Chars: %d\n", cfg.MsgParts*140)
|
|
fmt.Println()
|
|
fmt.Println("--- Test Parameters ---")
|
|
fmt.Printf(" Threads: %d\n", cfg.Threads)
|
|
fmt.Printf(" Target RPS: %d\n", cfg.RPS)
|
|
fmt.Printf(" Duration: %ds\n", cfg.DurationSec)
|
|
fmt.Printf(" Ramp-up: %ds\n", cfg.RampUpSec)
|
|
fmt.Println()
|
|
|
|
// Warnings
|
|
hasWarning := false
|
|
if cfg.RampUpSec >= cfg.DurationSec {
|
|
fmt.Println("WARNING: Ramp-up >= duration. The test will never reach full RPS.")
|
|
fmt.Printf(" Reduce LOAD_TEST_RAMP_UP_SECS (currently %ds) below duration (%ds).\n", cfg.RampUpSec, cfg.DurationSec)
|
|
hasWarning = true
|
|
}
|
|
// Estimate: need threads >= RPS * avg_latency_secs. Assume ~500ms avg latency.
|
|
estimatedLatency := 0.5
|
|
minThreads := int(float64(cfg.RPS)*estimatedLatency) + 1
|
|
if cfg.Threads < minThreads {
|
|
fmt.Printf("WARNING: Threads (%d) may be too low for target RPS (%d).\n", cfg.Threads, cfg.RPS)
|
|
fmt.Printf(" Recommended: at least %d threads (based on ~%.0fms estimated latency).\n", minThreads, estimatedLatency*1000)
|
|
hasWarning = true
|
|
}
|
|
if hasWarning {
|
|
fmt.Println()
|
|
}
|
|
|
|
fmt.Println("========================================")
|
|
fmt.Println()
|
|
|
|
client := sender.NewHTTPClient(cfg.Threads)
|
|
st := &stats.Stats{}
|
|
duration := time.Duration(cfg.DurationSec) * time.Second
|
|
rampUp := time.Duration(cfg.RampUpSec) * time.Second
|
|
|
|
// Ticker for live stats
|
|
statsTicker := time.NewTicker(500 * time.Millisecond)
|
|
defer statsTicker.Stop()
|
|
|
|
testStart := time.Now()
|
|
done := make(chan struct{})
|
|
|
|
// Live stats printer
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-statsTicker.C:
|
|
st.PrintLive(time.Since(testStart))
|
|
case <-done:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Rate limiter with ramp-up
|
|
var wg sync.WaitGroup
|
|
sem := make(chan struct{}, cfg.Threads)
|
|
|
|
targetRPS := cfg.RPS
|
|
ticker := time.NewTicker(time.Second / time.Duration(targetRPS))
|
|
defer ticker.Stop()
|
|
|
|
fmt.Println("Starting load test...")
|
|
fmt.Println()
|
|
|
|
for {
|
|
elapsed := time.Since(testStart)
|
|
if elapsed >= duration {
|
|
break
|
|
}
|
|
|
|
// Ramp-up: scale RPS linearly
|
|
currentRPS := targetRPS
|
|
if elapsed < rampUp && rampUp > 0 {
|
|
rampFraction := float64(elapsed) / float64(rampUp)
|
|
currentRPS = int(float64(targetRPS) * rampFraction)
|
|
if currentRPS < 1 {
|
|
currentRPS = 1
|
|
}
|
|
ticker.Reset(time.Second / time.Duration(currentRPS))
|
|
} else if elapsed >= rampUp {
|
|
ticker.Reset(time.Second / time.Duration(targetRPS))
|
|
}
|
|
|
|
<-ticker.C
|
|
|
|
sem <- struct{}{}
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
defer func() { <-sem }()
|
|
sender.SendSMS(client, cfg, st)
|
|
}()
|
|
}
|
|
|
|
// Wait for in-flight requests
|
|
fmt.Println("\n\nWaiting for in-flight requests to complete...")
|
|
wg.Wait()
|
|
close(done)
|
|
|
|
st.PrintFinalReport(time.Since(testStart))
|
|
}
|