204 lines
4.7 KiB
Go
204 lines
4.7 KiB
Go
|
|
package ratelimit
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"sync"
|
|||
|
|
"testing"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"github.com/hhs/camtalk/internal/config"
|
|||
|
|
"github.com/stretchr/testify/assert"
|
|||
|
|
"github.com/stretchr/testify/require"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func TestTokenBucket_Allow_FirstRequest(t *testing.T) {
|
|||
|
|
bucket := newTokenBucket(5, 0.2)
|
|||
|
|
|
|||
|
|
allowed, retryAfter := bucket.allow()
|
|||
|
|
|
|||
|
|
assert.True(t, allowed)
|
|||
|
|
assert.Equal(t, time.Duration(0), retryAfter)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestTokenBucket_Allow_ConsumeUntilEmpty(t *testing.T) {
|
|||
|
|
bucket := newTokenBucket(3, 0.2)
|
|||
|
|
|
|||
|
|
// 连续消耗 3 个令牌
|
|||
|
|
for i := 0; i < 3; i++ {
|
|||
|
|
allowed, _ := bucket.allow()
|
|||
|
|
assert.True(t, allowed, "request %d should be allowed", i+1)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 第 4 个请求应被拒绝
|
|||
|
|
allowed, retryAfter := bucket.allow()
|
|||
|
|
assert.False(t, allowed)
|
|||
|
|
assert.Greater(t, retryAfter, time.Duration(0))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestTokenBucket_Allow_RetryAfterCorrect(t *testing.T) {
|
|||
|
|
bucket := newTokenBucket(1, 1.0) // 每秒 1 个令牌
|
|||
|
|
|
|||
|
|
// 消耗唯一的令牌
|
|||
|
|
allowed, _ := bucket.allow()
|
|||
|
|
require.True(t, allowed)
|
|||
|
|
|
|||
|
|
// 立即再次请求应被拒绝
|
|||
|
|
allowed, retryAfter := bucket.allow()
|
|||
|
|
assert.False(t, allowed)
|
|||
|
|
// retryAfter 应约为 1 秒(允许一定误差)
|
|||
|
|
assert.InDelta(t, 1000, retryAfter.Milliseconds(), 100)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestTokenBucket_Allow_RefillAfterWait(t *testing.T) {
|
|||
|
|
bucket := newTokenBucket(2, 10.0) // 每秒 10 个令牌(每 100ms 一个)
|
|||
|
|
|
|||
|
|
// 消耗 2 个令牌
|
|||
|
|
bucket.allow()
|
|||
|
|
bucket.allow()
|
|||
|
|
|
|||
|
|
// 等待 150ms,应补充至少 1 个令牌
|
|||
|
|
time.Sleep(150 * time.Millisecond)
|
|||
|
|
|
|||
|
|
allowed, _ := bucket.allow()
|
|||
|
|
assert.True(t, allowed)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestTokenBucket_Allow_CapacityLimit(t *testing.T) {
|
|||
|
|
bucket := newTokenBucket(3, 1.0)
|
|||
|
|
|
|||
|
|
// 等待足够长时间让桶"溢出"
|
|||
|
|
time.Sleep(100 * time.Millisecond)
|
|||
|
|
|
|||
|
|
// 但最多只能消耗 capacity 个令牌
|
|||
|
|
for i := 0; i < 3; i++ {
|
|||
|
|
allowed, _ := bucket.allow()
|
|||
|
|
assert.True(t, allowed, "request %d should be allowed", i+1)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 第 4 个应被拒绝
|
|||
|
|
allowed, _ := bucket.allow()
|
|||
|
|
assert.False(t, allowed)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestTokenBucket_Allow_ConcurrentSafe(t *testing.T) {
|
|||
|
|
bucket := newTokenBucket(100, 10.0)
|
|||
|
|
var wg sync.WaitGroup
|
|||
|
|
successCount := 0
|
|||
|
|
var mu sync.Mutex
|
|||
|
|
|
|||
|
|
// 100 个并发请求
|
|||
|
|
for i := 0; i < 100; i++ {
|
|||
|
|
wg.Add(1)
|
|||
|
|
go func() {
|
|||
|
|
defer wg.Done()
|
|||
|
|
allowed, _ := bucket.allow()
|
|||
|
|
if allowed {
|
|||
|
|
mu.Lock()
|
|||
|
|
successCount++
|
|||
|
|
mu.Unlock()
|
|||
|
|
}
|
|||
|
|
}()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
wg.Wait()
|
|||
|
|
|
|||
|
|
// 应该正好 100 个成功(桶容量为 100)
|
|||
|
|
assert.Equal(t, 100, successCount)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestTokenBucket_Allow_ZeroCapacity(t *testing.T) {
|
|||
|
|
bucket := newTokenBucket(0, 1.0)
|
|||
|
|
|
|||
|
|
allowed, retryAfter := bucket.allow()
|
|||
|
|
assert.False(t, allowed)
|
|||
|
|
assert.Greater(t, retryAfter, time.Duration(0))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestTokenBucket_Allow_ZeroRate(t *testing.T) {
|
|||
|
|
bucket := newTokenBucket(1, 0.0)
|
|||
|
|
|
|||
|
|
// 第一个通过
|
|||
|
|
allowed, _ := bucket.allow()
|
|||
|
|
assert.True(t, allowed)
|
|||
|
|
|
|||
|
|
// 第二个被拒绝,且 retryAfter 应为无限大(实际上会很大)
|
|||
|
|
allowed, retryAfter := bucket.allow()
|
|||
|
|
assert.False(t, allowed)
|
|||
|
|
// rate=0 时,retryAfter 理论上无限大,实际会是一个很大的值
|
|||
|
|
assert.Greater(t, retryAfter, 1*time.Hour)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestMemoryLimiter_Allow_DifferentKeys(t *testing.T) {
|
|||
|
|
cfg := config.RateLimitConfig{
|
|||
|
|
Enabled: true,
|
|||
|
|
Query: config.BucketConfig{Capacity: 2, Rate: 1.0},
|
|||
|
|
}
|
|||
|
|
limiter := NewMemoryLimiter(cfg)
|
|||
|
|
defer limiter.Stop()
|
|||
|
|
|
|||
|
|
ctx := context.Background()
|
|||
|
|
|
|||
|
|
// user1 消耗 2 个令牌
|
|||
|
|
allowed, _ := limiter.Allow(ctx, "user1:query")
|
|||
|
|
assert.True(t, allowed)
|
|||
|
|
allowed, _ = limiter.Allow(ctx, "user1:query")
|
|||
|
|
assert.True(t, allowed)
|
|||
|
|
|
|||
|
|
// user1 第 3 个被拒绝
|
|||
|
|
allowed, _ = limiter.Allow(ctx, "user1:query")
|
|||
|
|
assert.False(t, allowed)
|
|||
|
|
|
|||
|
|
// user2 应该不受影响
|
|||
|
|
allowed, _ = limiter.Allow(ctx, "user2:query")
|
|||
|
|
assert.True(t, allowed)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestMemoryLimiter_Cleanup(t *testing.T) {
|
|||
|
|
cfg := config.RateLimitConfig{
|
|||
|
|
Enabled: true,
|
|||
|
|
Query: config.BucketConfig{Capacity: 1, Rate: 1.0},
|
|||
|
|
}
|
|||
|
|
limiter := NewMemoryLimiter(cfg)
|
|||
|
|
defer limiter.Stop()
|
|||
|
|
|
|||
|
|
ctx := context.Background()
|
|||
|
|
|
|||
|
|
// 创建一个桶
|
|||
|
|
limiter.Allow(ctx, "user1:query")
|
|||
|
|
|
|||
|
|
// 验证桶已创建
|
|||
|
|
limiter.mu.RLock()
|
|||
|
|
initialCount := len(limiter.buckets)
|
|||
|
|
limiter.mu.RUnlock()
|
|||
|
|
assert.Equal(t, 1, initialCount)
|
|||
|
|
|
|||
|
|
// 手动触发清理(模拟 10 分钟后)
|
|||
|
|
limiter.mu.Lock()
|
|||
|
|
for _, bucket := range limiter.buckets {
|
|||
|
|
bucket.mu.Lock()
|
|||
|
|
bucket.lastRefill = time.Now().Add(-11 * time.Minute)
|
|||
|
|
bucket.mu.Unlock()
|
|||
|
|
}
|
|||
|
|
limiter.mu.Unlock()
|
|||
|
|
|
|||
|
|
limiter.removeInactiveBuckets()
|
|||
|
|
|
|||
|
|
// 验证桶已被清理
|
|||
|
|
limiter.mu.RLock()
|
|||
|
|
finalCount := len(limiter.buckets)
|
|||
|
|
limiter.mu.RUnlock()
|
|||
|
|
assert.Equal(t, 0, finalCount)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestMemoryLimiter_Stop(t *testing.T) {
|
|||
|
|
cfg := config.RateLimitConfig{
|
|||
|
|
Enabled: true,
|
|||
|
|
Query: config.BucketConfig{Capacity: 1, Rate: 1.0},
|
|||
|
|
}
|
|||
|
|
limiter := NewMemoryLimiter(cfg)
|
|||
|
|
|
|||
|
|
// 多次调用 Stop 不应 panic
|
|||
|
|
limiter.Stop()
|
|||
|
|
limiter.Stop()
|
|||
|
|
}
|