Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions token/rate.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package token

import (
"errors"
"sync"
"time"
)

Expand All @@ -18,6 +19,7 @@ func (c realClock) Now() time.Time {
// Limiter implements a token bucket rate limiter. It allows a burst of
// requests up to capacity, then refills tokens at the specified rate per second.
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Limiter documentation should be updated to explicitly mention that it is safe for concurrent use by multiple goroutines. This is an important API property that users need to know about. Consider adding a sentence like "Limiter is safe for concurrent use by multiple goroutines." to the documentation.

Suggested change
// requests up to capacity, then refills tokens at the specified rate per second.
// requests up to capacity, then refills tokens at the specified rate per second.
// Limiter is safe for concurrent use by multiple goroutines.

Copilot uses AI. Check for mistakes.
type Limiter struct {
mu sync.Mutex
capacity, tokens, rate float64
lastRefillAt time.Time
clock clock
Expand Down Expand Up @@ -54,6 +56,9 @@ func NewLimiterWithClock(capacity, rate float64, clock clock) (*Limiter, error)
// available and returns true. If no tokens are available, it returns false
// without blocking.
func (lim *Limiter) Allow() bool {
lim.mu.Lock()
defer lim.mu.Unlock()

lim.refill()

if lim.tokens >= 1 {
Expand Down
54 changes: 54 additions & 0 deletions token/rate_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package token_test

import (
"sync"
"sync/atomic"
"testing"
"time"

Expand Down Expand Up @@ -106,3 +108,55 @@ func TestLimiter_Allow(t *testing.T) {
})
}
}

func TestLimiter_Allow_Concurrent(t *testing.T) {
lim, err := token.NewLimiter(100, 0)
require.NoError(t, err)

var (
allowed atomic.Int64
wg sync.WaitGroup
)

// Launch 200 goroutines, but only 100 should be allowed

for range 200 {
wg.Add(1)

go func() {
defer wg.Done()

if lim.Allow() {
allowed.Add(1)
}
}()
}

wg.Wait()

// With capacity 100 and rate 0, exactly 100 should be allowed
require.Equal(t, int64(100), allowed.Load(), "expected exactly 100 requests to be allowed")
}

func TestLimiter_Allow_ConcurrentWithRefill(t *testing.T) {
lim, err := token.NewLimiter(10, 1000)
require.NoError(t, err)

var wg sync.WaitGroup

// Hammer the limiter from multiple goroutines
for range 100 {
wg.Add(1)

go func() {
defer wg.Done()

for range 100 {
lim.Allow()
}
}()
}

wg.Wait()
// If we get here without race detector complaints, the test passes
}