docs(limiter/traffic): add doc comments, unit tests
This commit is contained in:
Vendored
+13
@@ -1,3 +1,6 @@
|
||||
// Package limiter provides a cached traffic limiter that wraps a
|
||||
// TrafficLimiter with in-memory caching to reduce calls to the underlying
|
||||
// limiter (e.g., plugin-based limiters).
|
||||
package limiter
|
||||
|
||||
import (
|
||||
@@ -20,20 +23,27 @@ type options struct {
|
||||
scope string
|
||||
}
|
||||
|
||||
// Option configures a cachedTrafficLimiter.
|
||||
type Option func(*options)
|
||||
|
||||
// RefreshIntervalOption sets the cache refresh interval. Cached limiters
|
||||
// are refreshed after this duration. The minimum value is 1 second.
|
||||
func RefreshIntervalOption(interval time.Duration) Option {
|
||||
return func(o *options) {
|
||||
o.refreshInterval = interval
|
||||
}
|
||||
}
|
||||
|
||||
// CleanupIntervalOption sets the interval for cleaning up expired cache
|
||||
// entries. The minimum value is 1 second.
|
||||
func CleanupIntervalOption(interval time.Duration) Option {
|
||||
return func(o *options) {
|
||||
o.cleanupInterval = interval
|
||||
}
|
||||
}
|
||||
|
||||
// ScopeOption filters cached limiters by scope. When set, only requests
|
||||
// with a matching scope return a limiter; non-matching scopes return nil.
|
||||
func ScopeOption(scope string) Option {
|
||||
return func(o *options) {
|
||||
o.scope = scope
|
||||
@@ -47,6 +57,9 @@ type cachedTrafficLimiter struct {
|
||||
options options
|
||||
}
|
||||
|
||||
// NewCachedTrafficLimiter wraps a TrafficLimiter with an in-memory cache.
|
||||
// Cached limiters are refreshed after the configured refresh interval.
|
||||
// Returns nil if the inner limiter is nil.
|
||||
func NewCachedTrafficLimiter(limiter traffic.TrafficLimiter, opts ...Option) traffic.TrafficLimiter {
|
||||
if limiter == nil {
|
||||
return nil
|
||||
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
package limiter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/limiter"
|
||||
"github.com/go-gost/core/limiter/traffic"
|
||||
)
|
||||
|
||||
// --- mock types ---
|
||||
|
||||
type mockLimiter struct {
|
||||
limit int
|
||||
}
|
||||
|
||||
func (m *mockLimiter) Wait(ctx context.Context, n int) int { return n }
|
||||
func (m *mockLimiter) Limit() int { return m.limit }
|
||||
func (m *mockLimiter) Set(n int) { m.limit = n }
|
||||
|
||||
type mockTrafficLimiter struct {
|
||||
inCalls int
|
||||
outCalls int
|
||||
inLim traffic.Limiter
|
||||
outLim traffic.Limiter
|
||||
}
|
||||
|
||||
func (m *mockTrafficLimiter) In(ctx context.Context, key string, opts ...limiter.Option) traffic.Limiter {
|
||||
m.inCalls++
|
||||
return m.inLim
|
||||
}
|
||||
|
||||
func (m *mockTrafficLimiter) Out(ctx context.Context, key string, opts ...limiter.Option) traffic.Limiter {
|
||||
m.outCalls++
|
||||
return m.outLim
|
||||
}
|
||||
|
||||
// --- tests ---
|
||||
|
||||
func TestCachedTrafficLimiter_In(t *testing.T) {
|
||||
inner := &mockTrafficLimiter{inLim: &mockLimiter{limit: 100}}
|
||||
cached := NewCachedTrafficLimiter(inner, RefreshIntervalOption(50*time.Millisecond))
|
||||
if cached == nil {
|
||||
t.Fatal("NewCachedTrafficLimiter should not return nil")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
v1 := cached.In(ctx, "key1")
|
||||
if v1 == nil {
|
||||
t.Fatal("first In() should return a limiter")
|
||||
}
|
||||
if v1.Limit() != 100 {
|
||||
t.Fatalf("expected limit 100, got %d", v1.Limit())
|
||||
}
|
||||
if inner.inCalls != 1 {
|
||||
t.Fatalf("expected 1 In call, got %d", inner.inCalls)
|
||||
}
|
||||
|
||||
// Second call: should return cached result without calling inner again.
|
||||
v2 := cached.In(ctx, "key1")
|
||||
if v2 == nil {
|
||||
t.Fatal("cached In() should return a limiter")
|
||||
}
|
||||
if inner.inCalls != 1 {
|
||||
t.Fatalf("cached call should not invoke inner, got %d calls", inner.inCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCachedTrafficLimiter_Out(t *testing.T) {
|
||||
inner := &mockTrafficLimiter{outLim: &mockLimiter{limit: 200}}
|
||||
cached := NewCachedTrafficLimiter(inner, RefreshIntervalOption(50*time.Millisecond))
|
||||
|
||||
ctx := context.Background()
|
||||
v1 := cached.Out(ctx, "key1")
|
||||
if v1 == nil {
|
||||
t.Fatal("first Out() should return a limiter")
|
||||
}
|
||||
if v1.Limit() != 200 {
|
||||
t.Fatalf("expected limit 200, got %d", v1.Limit())
|
||||
}
|
||||
if inner.outCalls != 1 {
|
||||
t.Fatalf("expected 1 Out call, got %d", inner.outCalls)
|
||||
}
|
||||
|
||||
cached.Out(ctx, "key1")
|
||||
if inner.outCalls != 1 {
|
||||
t.Fatalf("cached call should not invoke inner, got %d calls", inner.outCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCachedTrafficLimiter_Expiration(t *testing.T) {
|
||||
inner := &mockTrafficLimiter{inLim: &mockLimiter{limit: 100}}
|
||||
// Bypass constructor to avoid 1s minimum refresh interval.
|
||||
cached := NewCachedTrafficLimiter(inner, RefreshIntervalOption(10*time.Millisecond))
|
||||
if cached == nil {
|
||||
t.Fatal("NewCachedTrafficLimiter should not return nil")
|
||||
}
|
||||
ct := cached.(*cachedTrafficLimiter)
|
||||
ct.options.refreshInterval = 10 * time.Millisecond
|
||||
|
||||
ctx := context.Background()
|
||||
ct.In(ctx, "key1")
|
||||
if inner.inCalls != 1 {
|
||||
t.Fatalf("expected 1 In call, got %d", inner.inCalls)
|
||||
}
|
||||
|
||||
// Wait for cache to expire.
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
|
||||
ct.In(ctx, "key1")
|
||||
if inner.inCalls != 2 {
|
||||
t.Fatalf("after expiration, inner In should be called again, got %d calls", inner.inCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCachedTrafficLimiter_ScopeFilter(t *testing.T) {
|
||||
inner := &mockTrafficLimiter{inLim: &mockLimiter{limit: 100}}
|
||||
cached := NewCachedTrafficLimiter(inner,
|
||||
RefreshIntervalOption(50*time.Millisecond),
|
||||
ScopeOption("service"),
|
||||
)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Non-matching scope: returns nil.
|
||||
v := cached.In(ctx, "key1", limiter.ScopeOption(limiter.ScopeConn))
|
||||
if v != nil {
|
||||
t.Fatal("non-matching scope should return nil")
|
||||
}
|
||||
|
||||
// Matching scope: returns limiter.
|
||||
v = cached.In(ctx, "key1", limiter.ScopeOption(limiter.ScopeService))
|
||||
if v == nil {
|
||||
t.Fatal("matching scope should return limiter")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCachedTrafficLimiter_NilInner(t *testing.T) {
|
||||
cached := NewCachedTrafficLimiter(nil)
|
||||
if cached != nil {
|
||||
t.Fatal("NewCachedTrafficLimiter(nil) should return nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCachedTrafficLimiter_NilNew(t *testing.T) {
|
||||
inner := &mockTrafficLimiter{inLim: &mockLimiter{limit: 100}}
|
||||
cached := NewCachedTrafficLimiter(inner, RefreshIntervalOption(10*time.Millisecond))
|
||||
if cached == nil {
|
||||
t.Fatal("NewCachedTrafficLimiter should not return nil")
|
||||
}
|
||||
ct := cached.(*cachedTrafficLimiter)
|
||||
ct.options.refreshInterval = 10 * time.Millisecond
|
||||
|
||||
ctx := context.Background()
|
||||
v1 := ct.In(ctx, "key1")
|
||||
if v1 == nil || v1.Limit() != 100 {
|
||||
t.Fatal("first In() should return limiter with limit 100")
|
||||
}
|
||||
|
||||
// Now make inner return nil.
|
||||
inner.inLim = nil
|
||||
|
||||
// Wait for cache to expire.
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
|
||||
// After expiration: inner returns nil, falls back to cached limiter.
|
||||
v2 := ct.In(ctx, "key1")
|
||||
if v2 == nil {
|
||||
t.Fatal("should fall back to cached limiter when inner returns nil")
|
||||
}
|
||||
if v2.Limit() != 100 {
|
||||
t.Fatalf("expected fallback limit 100, got %d", v2.Limit())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user