165 lines
4.9 KiB
Go
165 lines
4.9 KiB
Go
package ratelimiter
|
|
|
|
import (
|
|
"errors"
|
|
"sync"
|
|
"testing"
|
|
"testing/synctest"
|
|
"time"
|
|
)
|
|
|
|
func TestNew_Panics(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
conf Config
|
|
}{
|
|
{"negative BurstLimit", Config{BurstLimit: -1, FillPeriod: time.Second, MaxWaitCount: 1}},
|
|
{"zero FillPeriod", Config{BurstLimit: 1, FillPeriod: 0, MaxWaitCount: 1}},
|
|
{"negative FillPeriod", Config{BurstLimit: 1, FillPeriod: -time.Second, MaxWaitCount: 1}},
|
|
{"negative MaxWaitCount", Config{BurstLimit: 1, FillPeriod: time.Second, MaxWaitCount: -1}},
|
|
{"zero BurstLimit and MaxWaitCount", Config{BurstLimit: 0, FillPeriod: time.Second, MaxWaitCount: 0}},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
defer func() {
|
|
if recover() == nil {
|
|
t.Error("expected panic, got none")
|
|
}
|
|
}()
|
|
New(tc.conf)
|
|
})
|
|
}
|
|
}
|
|
|
|
// limitN runs n goroutines concurrently, each calling l.Limit(), and returns
|
|
// their errors in an unordered slice.
|
|
func limitN(l *Limiter, n int) []error {
|
|
errs := make([]error, n)
|
|
var wg sync.WaitGroup
|
|
for i := range n {
|
|
wg.Add(1)
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
errs[i] = l.Limit()
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
return errs
|
|
}
|
|
|
|
func backoffCount(errs []error) int {
|
|
n := 0
|
|
for _, err := range errs {
|
|
if errors.Is(err, ErrBackoff) {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
// TestLimit_Burst: burst capacity is consumed by the first BurstLimit requests;
|
|
// excess requests are rejected without any waiting.
|
|
func TestLimit_Burst(t *testing.T) {
|
|
const burst, total = 5, 10
|
|
synctest.Test(t, func(t *testing.T) {
|
|
t0 := time.Now()
|
|
l := New(Config{BurstLimit: burst, FillPeriod: time.Second, MaxWaitCount: 0})
|
|
errs := limitN(l, total)
|
|
if got := backoffCount(errs); got != total-burst {
|
|
t.Errorf("backoffs: want %d, got %d", total-burst, got)
|
|
}
|
|
if elapsed := time.Since(t0); elapsed != 0 {
|
|
t.Errorf("elapsed: want 0, got %v", elapsed)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestLimit_Queue: up to MaxWaitCount requests queue and sleep; excess are rejected.
|
|
func TestLimit_Queue(t *testing.T) {
|
|
const maxWait, total = 5, 8
|
|
const period = time.Second
|
|
synctest.Test(t, func(t *testing.T) {
|
|
t0 := time.Now()
|
|
l := New(Config{BurstLimit: 0, FillPeriod: period, MaxWaitCount: maxWait})
|
|
errs := limitN(l, total)
|
|
if got := backoffCount(errs); got != total-maxWait {
|
|
t.Errorf("backoffs: want %d, got %d", total-maxWait, got)
|
|
}
|
|
// The last queued request sleeps maxWait*period.
|
|
if elapsed := time.Since(t0); elapsed != time.Duration(maxWait)*period {
|
|
t.Errorf("elapsed: want %v, got %v", time.Duration(maxWait)*period, elapsed)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestLimit_BurstAndQueue: burst requests pass immediately; overflow queues;
|
|
// excess are rejected.
|
|
func TestLimit_BurstAndQueue(t *testing.T) {
|
|
const burst, maxWait, total = 3, 5, 12
|
|
const period = time.Second
|
|
synctest.Test(t, func(t *testing.T) {
|
|
t0 := time.Now()
|
|
l := New(Config{BurstLimit: burst, FillPeriod: period, MaxWaitCount: maxWait})
|
|
errs := limitN(l, total)
|
|
if got := backoffCount(errs); got != total-(burst+maxWait) {
|
|
t.Errorf("backoffs: want %d, got %d", total-(burst+maxWait), got)
|
|
}
|
|
// Burst passes at T=0; the last queued request sleeps maxWait*period.
|
|
if elapsed := time.Since(t0); elapsed != time.Duration(maxWait)*period {
|
|
t.Errorf("elapsed: want %v, got %v", time.Duration(maxWait)*period, elapsed)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestLimit_Refill: capacity refills as time passes, allowing further requests.
|
|
func TestLimit_Refill(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
l := New(Config{BurstLimit: 2, FillPeriod: time.Second, MaxWaitCount: 0})
|
|
|
|
// Exhaust burst (waitTime: -2s → -1s → 0s).
|
|
if err := l.Limit(); err != nil {
|
|
t.Fatal("call 1:", err)
|
|
}
|
|
if err := l.Limit(); err != nil {
|
|
t.Fatal("call 2:", err)
|
|
}
|
|
// Next would push waitTime to 1s > 0 = maxWaitTime.
|
|
if err := l.Limit(); !errors.Is(err, ErrBackoff) {
|
|
t.Fatalf("call 3: want ErrBackoff, got %v", err)
|
|
}
|
|
|
|
// After 2s, two tokens have refilled (waitTime: 0s - 2s = -2s, clamped to -2s).
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// Burst is fully restored; same pattern repeats.
|
|
if err := l.Limit(); err != nil {
|
|
t.Fatal("call 4:", err)
|
|
}
|
|
if err := l.Limit(); err != nil {
|
|
t.Fatal("call 5:", err)
|
|
}
|
|
if err := l.Limit(); !errors.Is(err, ErrBackoff) {
|
|
t.Fatalf("call 6: want ErrBackoff, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestLimit_BurstCap: idle time does not accumulate credit beyond BurstLimit.
|
|
//
|
|
// After sleeping far longer than BurstLimit*FillPeriod, the limiter clamps
|
|
// waitTime back to minWaitTime, so only BurstLimit requests can burst through.
|
|
// Without the clamp, 10s of idle time at FillPeriod=1s would allow 10 requests.
|
|
func TestLimit_BurstCap(t *testing.T) {
|
|
const burst = 2
|
|
synctest.Test(t, func(t *testing.T) {
|
|
l := New(Config{BurstLimit: burst, FillPeriod: time.Second, MaxWaitCount: 0})
|
|
|
|
time.Sleep(10 * time.Second)
|
|
|
|
errs := limitN(l, burst+3)
|
|
if got := backoffCount(errs); got != 3 {
|
|
t.Errorf("backoffs: want 3, got %d", got)
|
|
}
|
|
})
|
|
}
|