Cast AID Agent 60785a52f5 Add SMS API load testing service for Cast
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>
2026-03-12 10:19:20 +01:00

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))
}