Skip to content

Commit

Permalink
cap counters at capacity (#116)
Browse files Browse the repository at this point in the history
  • Loading branch information
leeym authored Feb 21, 2025
1 parent 33c2b31 commit 7ab1808
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 0 deletions.
6 changes: 6 additions & 0 deletions slidingwindow.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package limiters
import (
"context"
"fmt"
"math"
"strconv"
"sync"
"time"
Expand Down Expand Up @@ -57,6 +58,11 @@ func (s *SlidingWindow) Limit(ctx context.Context) (time.Duration, error) {
return 0, err
}

// "prev" and "curr" are capped at "s.capacity + s.epsilon" using math.Ceil to round up any fractional values,
// ensuring that in the worst case, "total" can be slightly greater than "s.capacity".
prev = int64(math.Min(float64(prev), math.Ceil(float64(s.capacity)+s.epsilon)))
curr = int64(math.Min(float64(curr), math.Ceil(float64(s.capacity)+s.epsilon)))

total := float64(prev*int64(ttl))/float64(s.rate) + float64(curr)
if total-float64(s.capacity) >= s.epsilon {
var wait time.Duration
Expand Down
38 changes: 38 additions & 0 deletions slidingwindow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,44 @@ func (s *LimitersTestSuite) TestSlidingWindowOverflowAndWait() {
}
}

func (s *LimitersTestSuite) TestSlidingWindowOverflowAndNoWait() {
capacity := int64(3)
clock := newFakeClock()
for name, bucket := range s.slidingWindows(capacity, time.Second, clock, 1e-9) {
s.Run(name, func() {
clock.reset()

// Keep sending requests until it reaches the capacity.
for i := int64(0); i < capacity; i++ {
w, err := bucket.Limit(context.TODO())
s.Require().NoError(err)
s.Require().Equal(time.Duration(0), w)
clock.Sleep(time.Millisecond)
}

// The next request will be the first one to be rejected.
w, err := bucket.Limit(context.TODO())
s.Require().Equal(l.ErrLimitExhausted, err)
expected := clock.Now().Add(w)

// Send a few more requests, all of them should be told to come back at the same time.
for i := int64(0); i < capacity; i++ {
w, err = bucket.Limit(context.TODO())
s.Require().Equal(l.ErrLimitExhausted, err)
actual := clock.Now().Add(w)
s.Require().Equal(expected, actual, i)
clock.Sleep(time.Millisecond)
}

// Wait until it is ready.
clock.Sleep(expected.Sub(clock.Now()))
w, err = bucket.Limit(context.TODO())
s.Require().NoError(err)
s.Require().Equal(time.Duration(0), w)
})
}
}

func BenchmarkSlidingWindows(b *testing.B) {
s := new(LimitersTestSuite)
s.SetT(&testing.T{})
Expand Down

0 comments on commit 7ab1808

Please sign in to comment.