From 918fcd9a9f700a2b9273f6ac1dcf8dd585c4a677 Mon Sep 17 00:00:00 2001 From: Ihor Kalnytskyi Date: Wed, 8 Apr 2026 16:19:47 +0300 Subject: [PATCH] interceptor: add HTTP keep-alive toggle Persistent HTTP connections are not always helpful for all workloads. With synchronous/blocking backends, one interceptor can keep a connection open to a backend worker after a request finishes. Another interceptor may reuse that worker and wait until the first connection is closed, causing cross-replica blocking and higher latency. Add `KEDA_HTTP_DISABLE_KEEP_ALIVES` so operators can disable connection reuse when this behavior appears. Signed-off-by: Ihor Kalnytskyi --- CHANGELOG.md | 2 +- interceptor/config/timeouts.go | 2 ++ interceptor/proxy.go | 1 + interceptor/proxy_test.go | 47 ++++++++++++++++++++++++++++++++-- 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec3adda7c..d798695ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,7 @@ This changelog keeps track of work items that have been completed and are ready ### Improvements -- **General**: TODO ([#TODO](https://github.com/kedacore/http-add-on/issues/TODO)) +- **Interceptor**: Add `KEDA_HTTP_DISABLE_KEEP_ALIVES` to disable keep-alive connections from interceptor to backend services ([#1571](https://github.com/kedacore/http-add-on/pull/1571)) ### Fixes diff --git a/interceptor/config/timeouts.go b/interceptor/config/timeouts.go index 6cf189549..1f37c47ae 100644 --- a/interceptor/config/timeouts.go +++ b/interceptor/config/timeouts.go @@ -20,6 +20,8 @@ type Timeouts struct { WorkloadReplicas time.Duration `env:"KEDA_CONDITION_WAIT_TIMEOUT" envDefault:"20s"` // ForceHTTP2 toggles whether to try to force HTTP2 for all requests ForceHTTP2 bool `env:"KEDA_HTTP_FORCE_HTTP2" envDefault:"false"` + // DisableKeepAlives disables HTTP keep-alives for requests from interceptor to backend services. + DisableKeepAlives bool `env:"KEDA_HTTP_DISABLE_KEEP_ALIVES" envDefault:"false"` // MaxIdleConns is the max number of idle connections to keep in the // interceptor's internal connection pool across all backend services. // Increase this if you proxy to many unique backend services. diff --git a/interceptor/proxy.go b/interceptor/proxy.go index 3a379c06a..5ce13bce9 100644 --- a/interceptor/proxy.go +++ b/interceptor/proxy.go @@ -65,6 +65,7 @@ func BuildProxyHandler(cfg *ProxyHandlerConfig) http.Handler { Proxy: http.ProxyFromEnvironment, DialContext: dialFunc, ForceAttemptHTTP2: cfg.Timeouts.ForceHTTP2, + DisableKeepAlives: cfg.Timeouts.DisableKeepAlives, MaxIdleConns: cfg.Timeouts.MaxIdleConns, MaxIdleConnsPerHost: cfg.Timeouts.MaxIdleConnsPerHost, IdleConnTimeout: cfg.Timeouts.IdleConnTimeout, diff --git a/interceptor/proxy_test.go b/interceptor/proxy_test.go index 97b31fab0..82ec4c275 100644 --- a/interceptor/proxy_test.go +++ b/interceptor/proxy_test.go @@ -181,6 +181,47 @@ func TestProxyHandler_BackendReceivesCorrectRequest(t *testing.T) { } } +func TestProxyHandler_DisableKeepAlives(t *testing.T) { + var backendRequestedClose bool + h := newProxyTestHarness(t, harnessConfig{ + disableKeepAlives: true, + backendHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + backendRequestedClose = r.Close + w.WriteHeader(http.StatusOK) + }), + }) + + resp := h.doRequest(t, http.MethodGet, "/", testHost) + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Fatalf("status = %d, want %d", resp.StatusCode, http.StatusOK) + } + if !backendRequestedClose { + t.Error("expected backend request to set Connection: close when keep-alives are disabled") + } +} + +func TestProxyHandler_DefaultKeepAlivesEnabled(t *testing.T) { + var backendRequestedClose bool + h := newProxyTestHarness(t, harnessConfig{ + backendHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + backendRequestedClose = r.Close + w.WriteHeader(http.StatusOK) + }), + }) + + resp := h.doRequest(t, http.MethodGet, "/", testHost) + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Fatalf("status = %d, want %d", resp.StatusCode, http.StatusOK) + } + if backendRequestedClose { + t.Error("expected backend request to keep connections open when keep-alives are enabled") + } +} + func TestProxyHandler_UnknownHostReturnsError(t *testing.T) { h := newProxyTestHarness(t, harnessConfig{}) @@ -283,6 +324,7 @@ type harnessConfig struct { simulateColdStart bool tlsEnabled bool tracingEnabled bool + disableKeepAlives bool useBlockingQueue bool } @@ -369,8 +411,9 @@ func newProxyTestHarness(t *testing.T, cfg harnessConfig) *proxyTestHarness { RoutingTable: routingTable, Reader: fake.NewClientBuilder().WithScheme(kedacache.NewScheme()).Build(), Timeouts: config.Timeouts{ - WorkloadReplicas: 5 * time.Second, - ResponseHeader: 5 * time.Second, + WorkloadReplicas: 5 * time.Second, + ResponseHeader: 5 * time.Second, + DisableKeepAlives: cfg.disableKeepAlives, }, Serving: config.Serving{EnableColdStartHeader: cfg.enableColdStartHeader}, TLSConfig: tlsCfg,