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
// 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)
+7
View File
@@ -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{})
+28 -5
View File
@@ -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(
+8
View File
@@ -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
View File
@@ -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