mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-10-11 05:27:47 +00:00
fix(tests): configurable retry delays and circuit backoff overflow
Three bugs fixed: 1. **Test suite timeout** — The main-package test suite timed out at 120s because each failing request triggered 7 retries with exponential backoff (500ms base → 10s max ≈ 25.5s per request). Tests that trip the circuit breaker (3 requests × 25.5s = 76.5s) plus TestStartHTTPProxy_ErrorHandlerConfigured (another 25.5s) pushed cumulative time past the limit. Fix: add `Client.RetryBaseDelayMs` / `Client.RetryMaxDelayMs` config fields (env: CLIENT_RETRY_BASE_DELAY_MS / CLIENT_RETRY_MAX_DELAY_MS, defaults 500ms / 10000ms). Tests override these to 10ms so retry exhaustion completes in milliseconds. Full suite now runs in ~35s. 2. **effectiveCircuitBackoff float overflow** — On amd64, converting `+Inf` (float64) to `time.Duration` (int64) saturates to MinInt64, not MaxInt64. The existing "< base" guard then returned `base` instead of `maxSaneCircuitBackoff`, breaking the sanity ceiling for an uncapped config with a high trip count. Fix: detect `math.IsInf / math.IsNaN` before the cast and return the ceiling directly. 3. **TestStartHTTPProxy_ErrorHandlerConfigured** — standalone top-level test calling parseConfig() directly, bypassing the TestSuite override. Now sets and restores RetryBaseDelayMs/RetryMaxDelayMs explicitly. Co-authored-by: lukaszraczylo <[email protected]>
This commit is contained in:
co-authored by
lukaszraczylo
parent
aaa08dfbb2
commit
8d1518b961
@@ -392,6 +392,20 @@ func parseConfig() {
|
||||
}
|
||||
c.Client.MaxIdleConnDuration = idleDuration
|
||||
|
||||
// Retry backoff delays (milliseconds). Configurable so tests can set very
|
||||
// short values without burning seconds per attempt.
|
||||
retryBaseDelay := getDetailsFromEnv("CLIENT_RETRY_BASE_DELAY_MS", 500)
|
||||
if retryBaseDelay < 1 {
|
||||
retryBaseDelay = 500
|
||||
}
|
||||
c.Client.RetryBaseDelayMs = retryBaseDelay
|
||||
|
||||
retryMaxDelay := getDetailsFromEnv("CLIENT_RETRY_MAX_DELAY_MS", 10000)
|
||||
if retryMaxDelay < retryBaseDelay {
|
||||
retryMaxDelay = retryBaseDelay
|
||||
}
|
||||
c.Client.RetryMaxDelayMs = retryMaxDelay
|
||||
|
||||
// Secure by default: TLS verification is enabled unless explicitly disabled
|
||||
c.Client.DisableTLSVerify = getDetailsFromEnv("CLIENT_DISABLE_TLS_VERIFY", false)
|
||||
|
||||
|
||||
@@ -40,6 +40,13 @@ func (suite *Tests) SetupTest() {
|
||||
libpack_cache.New(5 * time.Minute)
|
||||
parseConfig()
|
||||
|
||||
// Use very short retry delays in tests so that retry-exhausting requests
|
||||
// (e.g. 7 attempts × backoff) complete in milliseconds instead of ~25s each.
|
||||
cfgMutex.Lock()
|
||||
cfg.Client.RetryBaseDelayMs = 10
|
||||
cfg.Client.RetryMaxDelayMs = 10
|
||||
cfgMutex.Unlock()
|
||||
|
||||
// Create context with cancel for cleanup
|
||||
suite.ctx, suite.cancel = context.WithCancel(context.Background())
|
||||
suite.apiDone = make(chan struct{})
|
||||
|
||||
@@ -477,7 +477,16 @@ func effectiveCircuitBackoff(base time.Duration, multiplier float64, maxBackoff
|
||||
}
|
||||
|
||||
exponent := float64(trips - 1)
|
||||
effective := time.Duration(float64(base) * math.Pow(multiplier, exponent))
|
||||
raw := float64(base) * math.Pow(multiplier, exponent)
|
||||
|
||||
// On amd64 the float64->int64 conversion for +Inf saturates to MinInt64
|
||||
// rather than MaxInt64, so the subsequent "< base" guard would return base
|
||||
// instead of the hard ceiling. Detect overflow before the cast.
|
||||
if math.IsInf(raw, 1) || math.IsNaN(raw) {
|
||||
return maxSaneCircuitBackoff
|
||||
}
|
||||
|
||||
effective := time.Duration(raw)
|
||||
if effective < base {
|
||||
// Overflow or float rounding pushed us below base; never shorten.
|
||||
return base
|
||||
@@ -937,17 +946,31 @@ func performProxyRequestWithEnhancedRetries(c fiber.Ctx, proxyURL string, backen
|
||||
var initialDelay time.Duration
|
||||
var maxDelayTime time.Duration
|
||||
|
||||
// Read per-request base and max delay from config; fall back to coded
|
||||
// defaults when zero (e.g. during very early startup before parseConfig).
|
||||
baseDelayMs := cfg.Client.RetryBaseDelayMs
|
||||
if baseDelayMs <= 0 {
|
||||
baseDelayMs = 500
|
||||
}
|
||||
maxDelayMs := cfg.Client.RetryMaxDelayMs
|
||||
if maxDelayMs <= 0 {
|
||||
maxDelayMs = 10000
|
||||
}
|
||||
|
||||
if backendUnhealthy {
|
||||
// Backend is known to be unhealthy, fail fast
|
||||
// Circuit breaker should handle this, so reduce retries
|
||||
attempts = 3
|
||||
initialDelay = 500 * time.Millisecond
|
||||
maxDelayTime = 5 * time.Second
|
||||
initialDelay = time.Duration(baseDelayMs) * time.Millisecond
|
||||
maxDelayTime = time.Duration(maxDelayMs/2) * time.Millisecond
|
||||
if maxDelayTime < initialDelay {
|
||||
maxDelayTime = initialDelay
|
||||
}
|
||||
} else {
|
||||
// Normal retry strategy
|
||||
attempts = 7
|
||||
initialDelay = 500 * time.Millisecond
|
||||
maxDelayTime = 10 * time.Second
|
||||
initialDelay = time.Duration(baseDelayMs) * time.Millisecond
|
||||
maxDelayTime = time.Duration(maxDelayMs) * time.Millisecond
|
||||
}
|
||||
|
||||
return retry.Do(
|
||||
|
||||
@@ -788,12 +788,18 @@ func TestStartHTTPProxy_ErrorHandlerConfigured(t *testing.T) {
|
||||
cfgMutex.Lock()
|
||||
origPort := cfg.Server.PortGraphQL
|
||||
origTimeout := cfg.Client.ClientTimeout
|
||||
origRetryBase := cfg.Client.RetryBaseDelayMs
|
||||
origRetryMax := cfg.Client.RetryMaxDelayMs
|
||||
origWS := cfg.WebSocket.Enable
|
||||
origAdmin := cfg.AdminDashboard.Enable
|
||||
origHost := cfg.Server.HostGraphQL
|
||||
origHostRO := cfg.Server.HostGraphQLReadOnly
|
||||
cfg.Server.PortGraphQL = port
|
||||
cfg.Client.ClientTimeout = 5
|
||||
// Use short retry delays so the 7-attempt retry loop for the unreachable
|
||||
// backend completes in milliseconds instead of ~25 seconds.
|
||||
cfg.Client.RetryBaseDelayMs = 10
|
||||
cfg.Client.RetryMaxDelayMs = 10
|
||||
cfg.WebSocket.Enable = false
|
||||
cfg.AdminDashboard.Enable = false
|
||||
// Point at a host that always refuses connections so proxyTheRequest
|
||||
@@ -807,6 +813,8 @@ func TestStartHTTPProxy_ErrorHandlerConfigured(t *testing.T) {
|
||||
cfgMutex.Lock()
|
||||
cfg.Server.PortGraphQL = origPort
|
||||
cfg.Client.ClientTimeout = origTimeout
|
||||
cfg.Client.RetryBaseDelayMs = origRetryBase
|
||||
cfg.Client.RetryMaxDelayMs = origRetryMax
|
||||
cfg.WebSocket.Enable = origWS
|
||||
cfg.AdminDashboard.Enable = origAdmin
|
||||
cfg.Server.HostGraphQL = origHost
|
||||
|
||||
+15
-13
@@ -42,19 +42,21 @@ type config struct {
|
||||
PerUserCacheDisabled bool // Disable per-user cache isolation (SECURITY RISK - not recommended)
|
||||
}
|
||||
Client struct {
|
||||
GQLClient *graphql.BaseClient
|
||||
FastProxyClient *fasthttp.Client
|
||||
JWTUserClaimPath string
|
||||
JWTRoleClaimPath string
|
||||
RoleFromHeader string
|
||||
proxy string
|
||||
ClientTimeout int
|
||||
MaxConnsPerHost int
|
||||
ReadTimeout int
|
||||
WriteTimeout int
|
||||
MaxIdleConnDuration int
|
||||
RoleRateLimit bool
|
||||
DisableTLSVerify bool
|
||||
GQLClient *graphql.BaseClient
|
||||
FastProxyClient *fasthttp.Client
|
||||
JWTUserClaimPath string
|
||||
JWTRoleClaimPath string
|
||||
RoleFromHeader string
|
||||
proxy string
|
||||
ClientTimeout int
|
||||
MaxConnsPerHost int
|
||||
ReadTimeout int
|
||||
WriteTimeout int
|
||||
MaxIdleConnDuration int
|
||||
RetryBaseDelayMs int // initial backoff delay in milliseconds (default 500)
|
||||
RetryMaxDelayMs int // maximum backoff delay in milliseconds (default 10000)
|
||||
RoleRateLimit bool
|
||||
DisableTLSVerify bool
|
||||
}
|
||||
Server struct {
|
||||
HostGraphQL string
|
||||
|
||||
Reference in New Issue
Block a user