Files
lukaszraczylo cedee416a8 improvements mid may 2025 (#24)
* 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
2025-09-30 18:27:33 +01:00

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)
}