From cfe882bf9f23138eb0b7f3ff4ac37b28b8c3abe5 Mon Sep 17 00:00:00 2001 From: Dmitry Verkhoturov Date: Wed, 19 Aug 2026 00:37:17 +0100 Subject: [PATCH] fix: measure request duration on the injectable clock Handler took its start time from nowFn but closed the measurement with time.Since, so the duration always came from the wall clock and no test could control it. TestBenchmarks_Handler therefore slept 50ms per request and asserted the result within a 10ms band, which failed roughly one run in ten on a loaded machine. Both ends read nowFn now, which is time.Now in production and carries the same monotonic reading time.Since uses, so the measurement is unchanged there. The test drives a fake clock instead of sleeping: the numbers are exact rather than banded, and the package tests run about five seconds faster. --- benchmarks.go | 4 ++- benchmarks_test.go | 63 ++++++++++++++++++++++++++++++---------------- 2 files changed, 44 insertions(+), 23 deletions(-) diff --git a/benchmarks.go b/benchmarks.go index e422d0e..23d2374 100644 --- a/benchmarks.go +++ b/benchmarks.go @@ -68,7 +68,9 @@ func (b *Benchmarks) Handler(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { st := b.nowFn() defer func() { - b.update(time.Since(st)) + // 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) } diff --git a/benchmarks_test.go b/benchmarks_test.go index ca2e5d3..bf02b34 100644 --- a/benchmarks_test.go +++ b/benchmarks_test.go @@ -131,14 +131,38 @@ func TestBenchmark_Cleanup(t *testing.T) { assert.Equal(t, 900, bench.data.Len()) } +// fakeClock drives Benchmarks without waiting on wall time +type fakeClock struct { + mu sync.Mutex + t time.Time +} + +func (c *fakeClock) now() time.Time { + c.mu.Lock() + defer c.mu.Unlock() + return c.t +} + +func (c *fakeClock) advance(d time.Duration) { + c.mu.Lock() + defer c.mu.Unlock() + c.t = c.t.Add(d) +} + func TestBenchmarks_Handler(t *testing.T) { + const reqDuration = 50 * time.Millisecond + + clk := &fakeClock{t: time.Date(2022, 5, 15, 0, 0, 0, 0, time.UTC)} + bench := NewBenchmarks() + bench.nowFn = clk.now + + // the handler burns fake time instead of sleeping, so every request measures exactly reqDuration handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + clk.advance(reqDuration) _, err := w.Write([]byte("blah blah")) - time.Sleep(time.Millisecond * 50) require.NoError(t, err) }) - bench := NewBenchmarks() ts := httptest.NewServer(bench.Handler(handler)) defer ts.Close() @@ -146,28 +170,23 @@ func TestBenchmarks_Handler(t *testing.T) { resp, err := ts.Client().Get(ts.URL) require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) + require.NoError(t, resp.Body.Close()) } - { - res := bench.Stats(time.Minute) - t.Logf("%+v", res) - assert.Equal(t, 100, res.Requests) - assert.True(t, res.RequestsSec <= 20 && res.RequestsSec >= 10) - assert.InDelta(t, 50000, res.AverageRespTime, 10000) - assert.InDelta(t, 50000, res.MinRespTime, 10000) - assert.InDelta(t, 50000, res.MaxRespTime, 10000) - assert.True(t, res.MaxRespTime >= res.MinRespTime) - } - - { - res := bench.Stats(time.Minute * 15) - t.Logf("%+v", res) - assert.Equal(t, 100, res.Requests) - assert.True(t, res.RequestsSec <= 20 && res.RequestsSec >= 10, res.RequestsSec) - assert.InDelta(t, 50000, res.AverageRespTime, 10000) - assert.InDelta(t, 50000, res.MinRespTime, 10000) - assert.InDelta(t, 50000, res.MaxRespTime, 10000) - assert.True(t, res.MaxRespTime >= res.MinRespTime) + // 100 requests of 50ms cover 5s of fake time, landing in six one-second buckets + const wantRespTime = int64(50000) // microseconds + wantRate := 100.0 / 6.0 + + for _, interval := range []time.Duration{time.Minute, 15 * time.Minute} { + t.Run(interval.String(), func(t *testing.T) { + res := bench.Stats(interval) + t.Logf("%+v", res) + assert.Equal(t, 100, res.Requests) + assert.InDelta(t, wantRate, res.RequestsSec, 0.001) + assert.Equal(t, wantRespTime, res.AverageRespTime) + assert.Equal(t, wantRespTime, res.MinRespTime) + assert.Equal(t, wantRespTime, res.MaxRespTime) + }) } }