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
|
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)
|
||||||
|
|
||||||
|
|||||||
@@ -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{})
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user