rest.CORS refuses "*" together with credentials since go-pkgz/rest#52, so the bump and the option have to land together: the option does not exist in v1.22.0 and the panic fires at construction, inside routes(), which makes it a startup failure rather than a request-time one. The wildcard stays. The comment widget is embedded on arbitrary third-party sites, so the set of origins is not knowable, which is why the escape hatch was asked for upstream instead of accepting the panic. What it costs is unchanged and now written next to the call: any site a signed-in user visits can read authenticated responses, so state-changing requests have to keep being protected by something other than the origin, X-XSRF-Token today. The example module is tidied in the same commit, as it reaches go-pkgz/rest through the replace directive and its indirect graph would otherwise keep the old pin and fail the readonly module check in CI. The bump also carries testify to v1.12.0, which drops go-spew and go-difflib from the module graph.
171 lines
4.7 KiB
Go
171 lines
4.7 KiB
Go
package rest
|
|
|
|
import (
|
|
"container/list"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
var maxTimeRangeDefault = time.Duration(15) * time.Minute
|
|
|
|
// Benchmarks is a basic benchmarking middleware collecting and reporting performance metrics
|
|
// It keeps track of the requests speeds and counts in 1s benchData buckets ,limiting the number of buckets
|
|
// to maxTimeRange. User can request the benchmark for any time duration. This is intended to be used
|
|
// for retrieving the benchmark data for the last minute, 5 minutes and up to maxTimeRange.
|
|
type Benchmarks struct {
|
|
st time.Time
|
|
data *list.List
|
|
lock sync.RWMutex
|
|
maxTimeRange time.Duration
|
|
|
|
nowFn func() time.Time // for testing only
|
|
}
|
|
|
|
type benchData struct {
|
|
// 1s aggregates
|
|
requests int
|
|
respTime time.Duration
|
|
minRespTime time.Duration
|
|
maxRespTime time.Duration
|
|
ts time.Time
|
|
}
|
|
|
|
// BenchmarkStats holds the stats for a given interval
|
|
type BenchmarkStats struct {
|
|
Requests int `json:"requests"`
|
|
RequestsSec float64 `json:"requests_sec"`
|
|
AverageRespTime int64 `json:"average_resp_time"`
|
|
MinRespTime int64 `json:"min_resp_time"`
|
|
MaxRespTime int64 `json:"max_resp_time"`
|
|
}
|
|
|
|
// NewBenchmarks creates a new benchmark middleware
|
|
func NewBenchmarks() *Benchmarks {
|
|
res := &Benchmarks{
|
|
st: time.Now(),
|
|
data: list.New(),
|
|
nowFn: time.Now,
|
|
maxTimeRange: maxTimeRangeDefault,
|
|
}
|
|
return res
|
|
}
|
|
|
|
// WithTimeRange sets the maximum time range for the benchmark to keep data for.
|
|
// Default is 15 minutes. The increase of this range will change memory utilization as each second of the range
|
|
// kept as benchData aggregate. The default means 15*60 = 900 seconds of data aggregate.
|
|
// Larger range allows for longer time periods to be benchmarked.
|
|
func (b *Benchmarks) WithTimeRange(maximum time.Duration) *Benchmarks {
|
|
b.lock.Lock()
|
|
defer b.lock.Unlock()
|
|
b.maxTimeRange = maximum
|
|
return b
|
|
}
|
|
|
|
// Handler calculates 1/5/10m request per second and allows to access those values
|
|
func (b *Benchmarks) Handler(next http.Handler) http.Handler {
|
|
|
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
|
st := b.nowFn()
|
|
defer func() {
|
|
// both ends come from nowFn so the measurement follows the same clock as the bucketing,
|
|
// which lets tests drive it instead of waiting on wall time
|
|
b.update(b.nowFn().Sub(st))
|
|
}()
|
|
next.ServeHTTP(w, r)
|
|
}
|
|
return http.HandlerFunc(fn)
|
|
}
|
|
|
|
func (b *Benchmarks) update(reqDuration time.Duration) {
|
|
now := b.nowFn().Truncate(time.Second)
|
|
|
|
b.lock.Lock()
|
|
defer b.lock.Unlock()
|
|
|
|
// keep maxTimeRange in the list, drop the rest
|
|
for e := b.data.Front(); e != nil; e = e.Next() {
|
|
if b.data.Front().Value.(benchData).ts.After(b.nowFn().Add(-b.maxTimeRange)) {
|
|
break
|
|
}
|
|
b.data.Remove(b.data.Front())
|
|
}
|
|
|
|
last := b.data.Back()
|
|
if last == nil || last.Value.(benchData).ts.Before(now) {
|
|
b.data.PushBack(benchData{requests: 1, respTime: reqDuration, ts: now,
|
|
minRespTime: reqDuration, maxRespTime: reqDuration})
|
|
return
|
|
}
|
|
|
|
bd := last.Value.(benchData)
|
|
bd.requests++
|
|
bd.respTime += reqDuration
|
|
|
|
if bd.minRespTime == 0 || reqDuration < bd.minRespTime {
|
|
bd.minRespTime = reqDuration
|
|
}
|
|
if bd.maxRespTime == 0 || reqDuration > bd.maxRespTime {
|
|
bd.maxRespTime = reqDuration
|
|
}
|
|
|
|
last.Value = bd
|
|
}
|
|
|
|
// Stats returns the current benchmark stats for the given duration
|
|
func (b *Benchmarks) Stats(interval time.Duration) BenchmarkStats {
|
|
if interval < time.Second { // minimum interval is 1s due to the bucket size
|
|
return BenchmarkStats{}
|
|
}
|
|
|
|
b.lock.RLock()
|
|
defer b.lock.RUnlock()
|
|
|
|
var (
|
|
requests int
|
|
respTime time.Duration
|
|
)
|
|
|
|
now := b.nowFn().Truncate(time.Second)
|
|
cutoff := now.Add(-interval)
|
|
stInterval, fnInterval := time.Time{}, time.Time{}
|
|
var minRespTime, maxRespTime time.Duration
|
|
count := 0
|
|
|
|
for e := b.data.Back(); e != nil && count < int(interval.Seconds()); e = e.Prev() { // reverse order
|
|
bd := e.Value.(benchData)
|
|
if bd.ts.Before(cutoff) {
|
|
break
|
|
}
|
|
|
|
if minRespTime == 0 || bd.minRespTime < minRespTime {
|
|
minRespTime = bd.minRespTime
|
|
}
|
|
if maxRespTime == 0 || bd.maxRespTime > maxRespTime {
|
|
maxRespTime = bd.maxRespTime
|
|
}
|
|
requests += bd.requests
|
|
respTime += bd.respTime
|
|
if fnInterval.IsZero() {
|
|
fnInterval = bd.ts.Add(time.Second)
|
|
}
|
|
stInterval = bd.ts
|
|
count++
|
|
}
|
|
|
|
if requests == 0 {
|
|
return BenchmarkStats{}
|
|
}
|
|
|
|
// ensure we calculate rate based on actual interval
|
|
actualInterval := max(fnInterval.Sub(stInterval), time.Second)
|
|
|
|
return BenchmarkStats{
|
|
Requests: requests,
|
|
RequestsSec: float64(requests) / actualInterval.Seconds(),
|
|
AverageRespTime: respTime.Microseconds() / int64(requests),
|
|
MinRespTime: minRespTime.Microseconds(),
|
|
MaxRespTime: maxRespTime.Microseconds(),
|
|
}
|
|
}
|