mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-06-05 23:03:48 +00:00
cedee416a8
* General improvements and bug fixes. * Improve tests coverage. * fixup! Improve tests coverage. * Update README.md with latest changes. * Fix the uint32 * Resolve issue with race condition for logging. * fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025 * Fix the test of the rate limiter * Add default ratelimit.json file * Update dependencies. * Significant refactor. * fixup! Significant refactor. * fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025 * fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025 * fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025 * fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025 * fixup! fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025 * fixup! fixup! fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025 * fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025 * fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025 * fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025
313 lines
7.0 KiB
Go
313 lines
7.0 KiB
Go
package main
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestNewRetryBudget(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config RetryBudgetConfig
|
|
}{
|
|
{
|
|
name: "default config",
|
|
config: RetryBudgetConfig{
|
|
TokensPerSecond: 10.0,
|
|
MaxTokens: 100,
|
|
Enabled: true,
|
|
},
|
|
},
|
|
{
|
|
name: "custom config",
|
|
config: RetryBudgetConfig{
|
|
TokensPerSecond: 50.0,
|
|
MaxTokens: 500,
|
|
Enabled: true,
|
|
},
|
|
},
|
|
{
|
|
name: "disabled config",
|
|
config: RetryBudgetConfig{
|
|
TokensPerSecond: 10.0,
|
|
MaxTokens: 100,
|
|
Enabled: false,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
logger := libpack_logger.New()
|
|
|
|
rb := NewRetryBudget(tt.config, logger)
|
|
|
|
assert.NotNil(t, rb)
|
|
assert.Equal(t, tt.config.Enabled, rb.enabled)
|
|
assert.Equal(t, tt.config.TokensPerSecond, rb.tokensPerSecond)
|
|
assert.Equal(t, int64(tt.config.MaxTokens), rb.maxTokens)
|
|
|
|
if tt.config.Enabled {
|
|
// Should start with max tokens
|
|
assert.Equal(t, int64(tt.config.MaxTokens), rb.currentTokens.Load())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRetryBudget_Allow(t *testing.T) {
|
|
t.Run("allow when enabled and tokens available", func(t *testing.T) {
|
|
config := RetryBudgetConfig{
|
|
TokensPerSecond: 10.0,
|
|
MaxTokens: 100,
|
|
Enabled: true,
|
|
}
|
|
|
|
rb := NewRetryBudget(config, libpack_logger.New())
|
|
|
|
// Should allow first request
|
|
allowed := rb.AllowRetry()
|
|
assert.True(t, allowed)
|
|
|
|
// Tokens should be decremented
|
|
assert.Less(t, rb.currentTokens.Load(), int64(100))
|
|
})
|
|
|
|
t.Run("deny when tokens exhausted", func(t *testing.T) {
|
|
config := RetryBudgetConfig{
|
|
TokensPerSecond: 10.0,
|
|
MaxTokens: 2,
|
|
Enabled: true,
|
|
}
|
|
|
|
rb := NewRetryBudget(config, libpack_logger.New())
|
|
|
|
// Consume all tokens
|
|
assert.True(t, rb.AllowRetry())
|
|
assert.True(t, rb.AllowRetry())
|
|
|
|
// Should deny when exhausted
|
|
assert.False(t, rb.AllowRetry())
|
|
|
|
stats := rb.GetStats()
|
|
assert.Greater(t, stats["denied_retries"].(int64), int64(0))
|
|
})
|
|
|
|
t.Run("always allow when disabled", func(t *testing.T) {
|
|
config := RetryBudgetConfig{
|
|
TokensPerSecond: 10.0,
|
|
MaxTokens: 0,
|
|
Enabled: false,
|
|
}
|
|
|
|
rb := NewRetryBudget(config, libpack_logger.New())
|
|
|
|
// Should always allow when disabled
|
|
for i := 0; i < 100; i++ {
|
|
assert.True(t, rb.AllowRetry())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRetryBudget_Refill(t *testing.T) {
|
|
t.Run("tokens refill over time", func(t *testing.T) {
|
|
config := RetryBudgetConfig{
|
|
TokensPerSecond: 100.0, // Fast refill for testing
|
|
MaxTokens: 100,
|
|
Enabled: true,
|
|
}
|
|
|
|
rb := NewRetryBudget(config, libpack_logger.New())
|
|
|
|
// Consume some tokens
|
|
for i := 0; i < 50; i++ {
|
|
rb.AllowRetry()
|
|
}
|
|
|
|
tokensBefore := rb.currentTokens.Load()
|
|
|
|
// Wait for refill (multiple refill cycles at 100ms each)
|
|
time.Sleep(300 * time.Millisecond)
|
|
|
|
tokensAfter := rb.currentTokens.Load()
|
|
|
|
// Tokens should have increased
|
|
assert.Greater(t, tokensAfter, tokensBefore)
|
|
})
|
|
|
|
t.Run("tokens don't exceed max", func(t *testing.T) {
|
|
config := RetryBudgetConfig{
|
|
TokensPerSecond: 100.0,
|
|
MaxTokens: 50,
|
|
Enabled: true,
|
|
}
|
|
|
|
rb := NewRetryBudget(config, libpack_logger.New())
|
|
|
|
// Wait for potential overflow
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
tokens := rb.currentTokens.Load()
|
|
assert.LessOrEqual(t, tokens, int64(50))
|
|
})
|
|
}
|
|
|
|
func TestRetryBudget_GetStats(t *testing.T) {
|
|
t.Run("tracks statistics correctly", func(t *testing.T) {
|
|
config := RetryBudgetConfig{
|
|
TokensPerSecond: 10.0,
|
|
MaxTokens: 5,
|
|
Enabled: true,
|
|
}
|
|
|
|
rb := NewRetryBudget(config, libpack_logger.New())
|
|
|
|
// Allow some requests
|
|
rb.AllowRetry()
|
|
rb.AllowRetry()
|
|
rb.AllowRetry()
|
|
|
|
// Consume all tokens to trigger denials
|
|
rb.AllowRetry()
|
|
rb.AllowRetry()
|
|
rb.AllowRetry() // Should be denied
|
|
rb.AllowRetry() // Should be denied
|
|
|
|
stats := rb.GetStats()
|
|
|
|
assert.Equal(t, true, stats["enabled"])
|
|
assert.Equal(t, 10.0, stats["tokens_per_sec"])
|
|
assert.Equal(t, int64(5), stats["max_tokens"])
|
|
assert.GreaterOrEqual(t, stats["current_tokens"].(int64), int64(0))
|
|
assert.Equal(t, int64(7), stats["total_attempts"])
|
|
assert.GreaterOrEqual(t, stats["denied_retries"].(int64), int64(2))
|
|
assert.Greater(t, stats["denial_rate_pct"].(float64), 0.0)
|
|
})
|
|
|
|
t.Run("stats when disabled", func(t *testing.T) {
|
|
config := RetryBudgetConfig{
|
|
TokensPerSecond: 10.0,
|
|
MaxTokens: 100,
|
|
Enabled: false,
|
|
}
|
|
|
|
rb := NewRetryBudget(config, libpack_logger.New())
|
|
|
|
stats := rb.GetStats()
|
|
|
|
assert.Equal(t, false, stats["enabled"])
|
|
assert.Equal(t, int64(0), stats["total_attempts"])
|
|
assert.Equal(t, int64(0), stats["denied_retries"])
|
|
})
|
|
}
|
|
|
|
func TestRetryBudget_Reset(t *testing.T) {
|
|
config := RetryBudgetConfig{
|
|
TokensPerSecond: 10.0,
|
|
MaxTokens: 10,
|
|
Enabled: true,
|
|
}
|
|
|
|
rb := NewRetryBudget(config, libpack_logger.New())
|
|
|
|
// Generate some activity
|
|
for i := 0; i < 15; i++ {
|
|
rb.AllowRetry()
|
|
}
|
|
|
|
statsBefore := rb.GetStats()
|
|
assert.Greater(t, statsBefore["total_attempts"].(int64), int64(0))
|
|
|
|
// Reset
|
|
rb.Reset()
|
|
|
|
statsAfter := rb.GetStats()
|
|
assert.Equal(t, int64(0), statsAfter["total_attempts"])
|
|
assert.Equal(t, int64(0), statsAfter["denied_retries"])
|
|
assert.Equal(t, int64(10), statsAfter["current_tokens"]) // Should reset to max
|
|
}
|
|
|
|
func TestRetryBudget_ConcurrentAccess(t *testing.T) {
|
|
config := RetryBudgetConfig{
|
|
TokensPerSecond: 100.0,
|
|
MaxTokens: 1000,
|
|
Enabled: true,
|
|
}
|
|
|
|
rb := NewRetryBudget(config, libpack_logger.New())
|
|
|
|
// Concurrent access test
|
|
done := make(chan bool)
|
|
goroutines := 100
|
|
requestsPerGoroutine := 10
|
|
|
|
for i := 0; i < goroutines; i++ {
|
|
go func() {
|
|
for j := 0; j < requestsPerGoroutine; j++ {
|
|
rb.AllowRetry()
|
|
}
|
|
done <- true
|
|
}()
|
|
}
|
|
|
|
// Wait for all goroutines
|
|
for i := 0; i < goroutines; i++ {
|
|
<-done
|
|
}
|
|
|
|
stats := rb.GetStats()
|
|
totalAttempts := stats["total_attempts"].(int64)
|
|
|
|
// Should have processed all requests
|
|
assert.Equal(t, int64(goroutines*requestsPerGoroutine), totalAttempts)
|
|
}
|
|
|
|
func TestRetryBudget_DenialRate(t *testing.T) {
|
|
config := RetryBudgetConfig{
|
|
TokensPerSecond: 1.0,
|
|
MaxTokens: 10,
|
|
Enabled: true,
|
|
}
|
|
|
|
rb := NewRetryBudget(config, libpack_logger.New())
|
|
|
|
// Consume all tokens
|
|
for i := 0; i < 10; i++ {
|
|
rb.AllowRetry()
|
|
}
|
|
|
|
// These should be denied
|
|
deniedCount := 0
|
|
for i := 0; i < 10; i++ {
|
|
if !rb.AllowRetry() {
|
|
deniedCount++
|
|
}
|
|
}
|
|
|
|
assert.Greater(t, deniedCount, 0)
|
|
|
|
stats := rb.GetStats()
|
|
denialRate := stats["denial_rate_pct"].(float64)
|
|
|
|
assert.Greater(t, denialRate, 0.0)
|
|
assert.LessOrEqual(t, denialRate, 100.0)
|
|
}
|
|
|
|
func TestRetryBudget_GlobalInstance(t *testing.T) {
|
|
config := RetryBudgetConfig{
|
|
TokensPerSecond: 10.0,
|
|
MaxTokens: 100,
|
|
Enabled: true,
|
|
}
|
|
|
|
rb := InitializeRetryBudget(config, libpack_logger.New())
|
|
assert.NotNil(t, rb)
|
|
|
|
// Should return the same instance
|
|
rb2 := GetRetryBudget()
|
|
assert.Equal(t, rb, rb2)
|
|
}
|