Skip to content

Commit

Permalink
switch to go 1.22 and update failed edge cases for benchmarks and fil…
Browse files Browse the repository at this point in the history
…e server listing
  • Loading branch information
umputun committed Dec 9, 2024
1 parent 9738b8e commit 99aa0b7
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 8 deletions.
18 changes: 15 additions & 3 deletions benchmarks.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,13 +124,18 @@ func (b *Benchmarks) Stats(interval time.Duration) BenchmarkStats {
respTime time.Duration
)

now := b.nowFn().Truncate(time.Second)
cutoff := now.Add(-interval)
stInterval, fnInterval := time.Time{}, time.Time{}
var minRespTime, maxRespTime time.Duration
for e := b.data.Back(); e != nil; e = e.Prev() { // reverse order
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(b.nowFn().Add(-interval)) {
if bd.ts.Before(cutoff) {
break
}

if minRespTime == 0 || bd.minRespTime < minRespTime {
minRespTime = bd.minRespTime
}
Expand All @@ -143,15 +148,22 @@ func (b *Benchmarks) Stats(interval time.Duration) BenchmarkStats {
fnInterval = bd.ts.Add(time.Second)
}
stInterval = bd.ts
count++
}

if requests == 0 {
return BenchmarkStats{}
}

// ensure we calculate rate based on actual interval
actualInterval := fnInterval.Sub(stInterval)
if actualInterval < time.Second {
actualInterval = time.Second
}

return BenchmarkStats{
Requests: requests,
RequestsSec: float64(requests) / (fnInterval.Sub(stInterval).Seconds()),
RequestsSec: float64(requests) / actualInterval.Seconds(),
AverageRespTime: respTime.Microseconds() / int64(requests),
MinRespTime: minRespTime.Microseconds(),
MaxRespTime: maxRespTime.Microseconds(),
Expand Down
146 changes: 146 additions & 0 deletions benchmarks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package rest
import (
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"

Expand Down Expand Up @@ -169,3 +170,148 @@ func TestBenchmarks_Handler(t *testing.T) {
assert.True(t, res.MaxRespTime >= res.MinRespTime)
}
}

func TestBenchmark_ConcurrentAccess(t *testing.T) {
bench := NewBenchmarks()
var wg sync.WaitGroup

// simulate concurrent updates
for i := 0; i < 100; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
bench.update(time.Duration(i) * time.Millisecond)
}(i)
}

// simulate concurrent stats reads while updating
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
stats := bench.Stats(time.Minute)
require.GreaterOrEqual(t, stats.Requests, 0)
}()
}

wg.Wait()

stats := bench.Stats(time.Minute)
assert.Equal(t, 100, stats.Requests)
}

func TestBenchmark_EdgeCases(t *testing.T) {
bench := NewBenchmarks()

t.Run("zero duration", func(t *testing.T) {
bench.update(0)
stats := bench.Stats(time.Minute)
assert.Equal(t, int64(0), stats.MinRespTime)
assert.Equal(t, int64(0), stats.MaxRespTime)
})

t.Run("very large duration", func(t *testing.T) {
bench.update(time.Hour)
stats := bench.Stats(time.Minute)
assert.Equal(t, time.Hour.Microseconds(), stats.MaxRespTime)
})

t.Run("negative stats interval", func(t *testing.T) {
stats := bench.Stats(-time.Minute)
assert.Equal(t, BenchmarkStats{}, stats)
})
}

func TestBenchmark_TimeWindowBoundaries(t *testing.T) {
bench := NewBenchmarks()
now := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)

bench.nowFn = func() time.Time { return now }

// add data points exactly at minute boundaries
for i := 0; i < 120; i++ {
bench.nowFn = func() time.Time {
return now.Add(time.Duration(i) * time.Second)
}
bench.update(time.Millisecond * 50)
}

tests := []struct {
name string
interval time.Duration
want int // expected number of requests
}{
{"exact minute", time.Minute, 60},
{"30 seconds", time.Second * 30, 30},
{"90 seconds", time.Second * 90, 90},
{"2 minutes", time.Minute * 2, 120},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stats := bench.Stats(tt.interval)
assert.Equal(t, tt.want, stats.Requests, "interval %v should have %d requests", tt.interval, tt.want)
})
}
}

func TestBenchmark_CustomTimeRange(t *testing.T) {
tests := []struct {
name string
timeRange time.Duration
dataPoints int
wantKept int
}{
{"1 minute range", time.Minute, 120, 60},
{"5 minute range", time.Minute * 5, 400, 300},
{"custom 45s range", time.Second * 45, 100, 45},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bench := NewBenchmarks().WithTimeRange(tt.timeRange)
now := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)

// Add data points
for i := 0; i < tt.dataPoints; i++ {
bench.nowFn = func() time.Time {
return now.Add(time.Duration(i) * time.Second)
}
bench.update(time.Millisecond * 50)
}

assert.Equal(t, tt.wantKept, bench.data.Len(),
"should keep only %d data points for %v time range",
tt.wantKept, tt.timeRange)
})
}
}

func TestBenchmark_VariableLoad(t *testing.T) {
bench := NewBenchmarks()
now := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)

// simulate variable load pattern
patterns := []struct {
count int
duration time.Duration
}{
{10, time.Millisecond * 10}, // fast responses
{5, time.Millisecond * 100}, // medium responses
{2, time.Millisecond * 1000}, // slow responses
}

for i, p := range patterns {
bench.nowFn = func() time.Time {
return now.Add(time.Duration(i) * time.Second)
}
for j := 0; j < p.count; j++ {
bench.update(p.duration)
}
}

stats := bench.Stats(time.Minute)
assert.Equal(t, 17, stats.Requests) // total requests across all patterns
assert.Equal(t, int64(1000*1000), stats.MaxRespTime) // should be the max (1000ms = 1_000_000 microseconds)
assert.Equal(t, int64(10*1000), stats.MinRespTime) // should be the min (10ms = 10_000 microseconds)
}
4 changes: 3 additions & 1 deletion file_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ func TestFileServerWithListing(t *testing.T) {
assert.Equal(t, http.StatusOK, resp.StatusCode)
msg, err := io.ReadAll(resp.Body)
require.NoError(t, err)
exp := `<pre>
exp := `<!doctype html>
<meta name="viewport" content="width=device-width">
<pre>
<a href="f1.html">f1.html</a>
<a href="f2.html">f2.html</a>
</pre>
Expand Down
8 changes: 6 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
module github.com/go-pkgz/rest

go 1.21
go 1.22

require github.com/stretchr/testify v1.8.4
require (
github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.30.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.28.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
8 changes: 6 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down

0 comments on commit 99aa0b7

Please sign in to comment.