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:
copilot-swe-agent[bot]
2026-08-14 15:58:10 +00:00
committed by GitHub
co-authored by lukaszraczylo
parent aaa08dfbb2
commit 8d1518b961
5 changed files with 72 additions and 18 deletions
+14
View File
@@ -392,6 +392,20 @@ func parseConfig() {
} }
c.Client.MaxIdleConnDuration = idleDuration 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 // Secure by default: TLS verification is enabled unless explicitly disabled
c.Client.DisableTLSVerify = getDetailsFromEnv("CLIENT_DISABLE_TLS_VERIFY", false) c.Client.DisableTLSVerify = getDetailsFromEnv("CLIENT_DISABLE_TLS_VERIFY", false)
+7
View File
@@ -40,6 +40,13 @@ func (suite *Tests) SetupTest() {
libpack_cache.New(5 * time.Minute) libpack_cache.New(5 * time.Minute)
parseConfig() 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 // Create context with cancel for cleanup
suite.ctx, suite.cancel = context.WithCancel(context.Background()) suite.ctx, suite.cancel = context.WithCancel(context.Background())
suite.apiDone = make(chan struct{}) suite.apiDone = make(chan struct{})
+28 -5
View File
@@ -477,7 +477,16 @@ func effectiveCircuitBackoff(base time.Duration, multiplier float64, maxBackoff
} }
exponent := float64(trips - 1) 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 { if effective < base {
// Overflow or float rounding pushed us below base; never shorten. // Overflow or float rounding pushed us below base; never shorten.
return base return base
@@ -937,17 +946,31 @@ func performProxyRequestWithEnhancedRetries(c fiber.Ctx, proxyURL string, backen
var initialDelay time.Duration var initialDelay time.Duration
var maxDelayTime 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 { if backendUnhealthy {
// Backend is known to be unhealthy, fail fast // Backend is known to be unhealthy, fail fast
// Circuit breaker should handle this, so reduce retries // Circuit breaker should handle this, so reduce retries
attempts = 3 attempts = 3
initialDelay = 500 * time.Millisecond initialDelay = time.Duration(baseDelayMs) * time.Millisecond
maxDelayTime = 5 * time.Second maxDelayTime = time.Duration(maxDelayMs/2) * time.Millisecond
if maxDelayTime < initialDelay {
maxDelayTime = initialDelay
}
} else { } else {
// Normal retry strategy // Normal retry strategy
attempts = 7 attempts = 7
initialDelay = 500 * time.Millisecond initialDelay = time.Duration(baseDelayMs) * time.Millisecond
maxDelayTime = 10 * time.Second maxDelayTime = time.Duration(maxDelayMs) * time.Millisecond
} }
return retry.Do( return retry.Do(
+8
View File
@@ -788,12 +788,18 @@ func TestStartHTTPProxy_ErrorHandlerConfigured(t *testing.T) {
cfgMutex.Lock() cfgMutex.Lock()
origPort := cfg.Server.PortGraphQL origPort := cfg.Server.PortGraphQL
origTimeout := cfg.Client.ClientTimeout origTimeout := cfg.Client.ClientTimeout
origRetryBase := cfg.Client.RetryBaseDelayMs
origRetryMax := cfg.Client.RetryMaxDelayMs
origWS := cfg.WebSocket.Enable origWS := cfg.WebSocket.Enable
origAdmin := cfg.AdminDashboard.Enable origAdmin := cfg.AdminDashboard.Enable
origHost := cfg.Server.HostGraphQL origHost := cfg.Server.HostGraphQL
origHostRO := cfg.Server.HostGraphQLReadOnly origHostRO := cfg.Server.HostGraphQLReadOnly
cfg.Server.PortGraphQL = port cfg.Server.PortGraphQL = port
cfg.Client.ClientTimeout = 5 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.WebSocket.Enable = false
cfg.AdminDashboard.Enable = false cfg.AdminDashboard.Enable = false
// Point at a host that always refuses connections so proxyTheRequest // Point at a host that always refuses connections so proxyTheRequest
@@ -807,6 +813,8 @@ func TestStartHTTPProxy_ErrorHandlerConfigured(t *testing.T) {
cfgMutex.Lock() cfgMutex.Lock()
cfg.Server.PortGraphQL = origPort cfg.Server.PortGraphQL = origPort
cfg.Client.ClientTimeout = origTimeout cfg.Client.ClientTimeout = origTimeout
cfg.Client.RetryBaseDelayMs = origRetryBase
cfg.Client.RetryMaxDelayMs = origRetryMax
cfg.WebSocket.Enable = origWS cfg.WebSocket.Enable = origWS
cfg.AdminDashboard.Enable = origAdmin cfg.AdminDashboard.Enable = origAdmin
cfg.Server.HostGraphQL = origHost cfg.Server.HostGraphQL = origHost
+15 -13
View File
@@ -42,19 +42,21 @@ type config struct {
PerUserCacheDisabled bool // Disable per-user cache isolation (SECURITY RISK - not recommended) PerUserCacheDisabled bool // Disable per-user cache isolation (SECURITY RISK - not recommended)
} }
Client struct { Client struct {
GQLClient *graphql.BaseClient GQLClient *graphql.BaseClient
FastProxyClient *fasthttp.Client FastProxyClient *fasthttp.Client
JWTUserClaimPath string JWTUserClaimPath string
JWTRoleClaimPath string JWTRoleClaimPath string
RoleFromHeader string RoleFromHeader string
proxy string proxy string
ClientTimeout int ClientTimeout int
MaxConnsPerHost int MaxConnsPerHost int
ReadTimeout int ReadTimeout int
WriteTimeout int WriteTimeout int
MaxIdleConnDuration int MaxIdleConnDuration int
RoleRateLimit bool RetryBaseDelayMs int // initial backoff delay in milliseconds (default 500)
DisableTLSVerify bool RetryMaxDelayMs int // maximum backoff delay in milliseconds (default 10000)
RoleRateLimit bool
DisableTLSVerify bool
} }
Server struct { Server struct {
HostGraphQL string HostGraphQL string