From 27cb4d8aaafce42d26200a5554c9573777986ccf Mon Sep 17 00:00:00 2001 From: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com> Date: Mon, 8 Jun 2026 09:10:25 +0900 Subject: [PATCH] Expose disposeInactivePoolsInBackground for HttpClient connection pool Add configuration properties to expose Reactor Netty's ConnectionProvider.Builder#disposeInactivePoolsInBackground(disposeInterval, poolInactivity). Unlike the existing eviction-interval (which evicts idle connections), this disposes connection pools that have been entirely inactive, preventing unbounded growth of the channelPools map when downstream instances churn (e.g. Kubernetes pods rolling, so the remote address changes). Both spring.cloud.gateway.server.webflux.httpclient.pool.inactive-pool-dispose-interval and ...pool.pool-inactivity must be set for the background disposal to be enabled; otherwise behavior is unchanged. Fixes gh-4165 Signed-off-by: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com> --- .../gateway/config/HttpClientFactory.java | 5 +++ .../gateway/config/HttpClientProperties.java | 33 ++++++++++++++++++- .../config/GatewayAutoConfigurationTests.java | 4 +++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/config/HttpClientFactory.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/config/HttpClientFactory.java index 84e29bb3f..8dcf0e3bf 100644 --- a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/config/HttpClientFactory.java +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/config/HttpClientFactory.java @@ -192,6 +192,11 @@ protected ConnectionProvider buildConnectionProvider(HttpClientProperties proper builder.evictInBackground(pool.getEvictionInterval()); builder.metrics(pool.isMetrics()); + if (pool.getInactivePoolDisposeInterval() != null && pool.getPoolInactivity() != null) { + builder.disposeInactivePoolsInBackground(pool.getInactivePoolDisposeInterval(), + pool.getPoolInactivity()); + } + // Define the pool leasing strategy if (pool.getLeasingStrategy() == FIFO) { builder.fifo(); diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/config/HttpClientProperties.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/config/HttpClientProperties.java index 9281ead33..a7892fa8b 100644 --- a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/config/HttpClientProperties.java +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/config/HttpClientProperties.java @@ -208,6 +208,20 @@ public static class Pool { */ private boolean metrics = false; + /** + * Interval at which connection pools are regularly checked in the background, and + * the pools that have been inactive for longer than {@code poolInactivity} are + * disposed. If NULL, inactive pools are not disposed in the background. + */ + private Duration inactivePoolDisposeInterval = null; + + /** + * Duration of inactivity after which a connection pool is considered inactive and + * eligible to be disposed by the background check. Only applies when + * {@code inactivePoolDisposeInterval} is set. + */ + private Duration poolInactivity = null; + /** * Configures the leasing strategy for the pool (fifo or lifo), defaults to FIFO * which is Netty's default. @@ -278,6 +292,22 @@ public void setMetrics(boolean metrics) { this.metrics = metrics; } + public Duration getInactivePoolDisposeInterval() { + return inactivePoolDisposeInterval; + } + + public void setInactivePoolDisposeInterval(Duration inactivePoolDisposeInterval) { + this.inactivePoolDisposeInterval = inactivePoolDisposeInterval; + } + + public Duration getPoolInactivity() { + return poolInactivity; + } + + public void setPoolInactivity(Duration poolInactivity) { + this.poolInactivity = poolInactivity; + } + public LeasingStrategy getLeasingStrategy() { return leasingStrategy; } @@ -291,7 +321,8 @@ public String toString() { return "Pool{" + "type=" + type + ", name='" + name + '\'' + ", maxConnections=" + maxConnections + ", acquireTimeout=" + acquireTimeout + ", maxIdleTime=" + maxIdleTime + ", maxLifeTime=" + maxLifeTime + ", evictionInterval=" + evictionInterval + ", metrics=" + metrics - + ", leasingStrategy=" + leasingStrategy + '}'; + + ", inactivePoolDisposeInterval=" + inactivePoolDisposeInterval + ", poolInactivity=" + + poolInactivity + ", leasingStrategy=" + leasingStrategy + '}'; } public enum PoolType { diff --git a/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java index 095066d68..f0acfdb1e 100644 --- a/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java +++ b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java @@ -129,6 +129,8 @@ public void nettyHttpClientConfigured() { "spring.cloud.gateway.server.webflux.httpclient.pool.eviction-interval=10s", "spring.cloud.gateway.server.webflux.httpclient.pool.type=fixed", "spring.cloud.gateway.server.webflux.httpclient.pool.metrics=true", + "spring.cloud.gateway.server.webflux.httpclient.pool.inactive-pool-dispose-interval=30s", + "spring.cloud.gateway.server.webflux.httpclient.pool.pool-inactivity=5m", "spring.cloud.gateway.server.webflux.httpclient.pool.leasing-strategy=lifo", "spring.cloud.gateway.server.webflux.httpclient.compression=true", "spring.cloud.gateway.server.webflux.httpclient.wiretap=true", @@ -145,6 +147,8 @@ public void nettyHttpClientConfigured() { assertThat(properties.isCompression()).isEqualTo(true); assertThat(properties.getPool().getEvictionInterval()).hasSeconds(10); assertThat(properties.getPool().isMetrics()).isEqualTo(true); + assertThat(properties.getPool().getInactivePoolDisposeInterval()).hasSeconds(30); + assertThat(properties.getPool().getPoolInactivity()).hasMinutes(5); assertThat(properties.getPool().getLeasingStrategy()).isEqualTo(LeasingStrategy.LIFO); assertThat(properties.getSsl().getSslBundle()).isEqualTo("mybundle");