diff --git a/internal/sarin/response.go b/internal/sarin/response.go index fef21a3..f5d8581 100644 --- a/internal/sarin/response.go +++ b/internal/sarin/response.go @@ -17,6 +17,14 @@ import ( const DefaultResponseDurationAccuracy uint32 = 1 const DefaultResponseColumnMaxWidth = 50 +// maxResponseKeys bounds the distinct response keys, so error messages carrying +// per-request text cannot grow the map without limit. It stays above the number of +// HTTP status codes, so real ones are never merged. +const maxResponseKeys = 1000 + +// otherResponseKey collects everything that arrives after the key limit is reached. +const otherResponseKey = "other" + // Duration wraps time.Duration to provide consistent JSON/YAML marshaling as human-readable strings. type Duration time.Duration @@ -95,6 +103,11 @@ func (data *SarinResponseData) Add(responseKey string, responseTime time.Duratio defer data.Unlock() response, ok := data.Responses[responseKey] + if !ok && len(data.Responses) >= maxResponseKeys { + responseKey = otherResponseKey + response, ok = data.Responses[responseKey] + } + if !ok { data.Responses[responseKey] = &Response{ durations: map[time.Duration]uint64{ diff --git a/internal/sarin/worker.go b/internal/sarin/worker.go index 3b4e444..2889c73 100644 --- a/internal/sarin/worker.go +++ b/internal/sarin/worker.go @@ -1,12 +1,14 @@ package sarin import ( + "errors" "strconv" "sync/atomic" "time" "github.com/valyala/fasthttp" "go.aykhans.me/sarin/internal/script" + "go.aykhans.me/sarin/internal/types" ) const dryRunResponseKey = "dry-run" @@ -29,6 +31,16 @@ func statusCodeToString(code int) string { return strconv.Itoa(code) } +// requestErrorKey keys a generation failure by its source instead of its message. +// A script picks its own URLs and error texts, so the message would mint a new +// stats entry per request. The full message still goes to the runtime log. +func requestErrorKey(err error) string { + if chainErr, ok := errors.AsType[types.ScriptChainError](err); ok { + return chainErr.EngineType + " script[" + strconv.Itoa(chainErr.Index) + "] error" + } + return "request generation error" +} + func (s sarin) Worker( jobs <-chan struct{}, counter *atomic.Uint64, @@ -112,7 +124,7 @@ func (s sarin) workerStatsWithDynamic( clientIndex := nextClientIndex() bindScriptProxy(clientIndex) if err := requestGenerator(req); err != nil { - s.responses.Add(err.Error(), 0) + s.responses.Add(requestErrorKey(err), 0) sendLog(runtimeLogLevelError, err.Error()) counter.Add(1) continue @@ -144,8 +156,9 @@ func (s sarin) workerStatsWithStatic( ) { if err := requestGenerator(req); err != nil { // Static request generation failed, so record all jobs as errors + errorKey := requestErrorKey(err) for range jobs { - s.responses.Add(err.Error(), 0) + s.responses.Add(errorKey, 0) sendLog(runtimeLogLevelError, err.Error()) counter.Add(1) } @@ -241,7 +254,7 @@ func (s sarin) workerDryRunStatsWithDynamic( bindScriptProxy(nextClientIndex()) startTime := time.Now() if err := requestGenerator(req); err != nil { - s.responses.Add(err.Error(), time.Since(startTime)) + s.responses.Add(requestErrorKey(err), time.Since(startTime)) sendLog(runtimeLogLevelError, err.Error()) counter.Add(1) continue @@ -260,8 +273,9 @@ func (s sarin) workerDryRunStatsWithStatic( ) { if err := requestGenerator(req); err != nil { // Static request generation failed, so record all jobs as errors + errorKey := requestErrorKey(err) for range jobs { - s.responses.Add(err.Error(), 0) + s.responses.Add(errorKey, 0) sendLog(runtimeLogLevelError, err.Error()) counter.Add(1) }