Skip to content

gh-4569: Deterministically close shared HTTP client on TransportClientFactory#shutdown() - #4585

Open
PRAHLAD09-dev wants to merge 1 commit into
spring-cloud:mainfrom
PRAHLAD09-dev:fix/4569-eureka-deregistration-shutdown
Open

gh-4569: Deterministically close shared HTTP client on TransportClientFactory#shutdown()#4585
PRAHLAD09-dev wants to merge 1 commit into
spring-cloud:mainfrom
PRAHLAD09-dev:fix/4569-eureka-deregistration-shutdown

Conversation

@PRAHLAD09-dev

Copy link
Copy Markdown

Fixes #4569

Problem

RestClientTransportClientFactory.newClient() builds a brand-new CloseableHttpClient (with its own dedicated connection pool) on every single call. Neither TransportClientFactory#shutdown() nor EurekaHttpClient#shutdown() ever closes these clients - cleanup relied entirely on GC. During graceful shutdown, the final unregister() DELETE call (made from DiscoveryClient.shutdown(), right after cancelScheduledTasks()) can hit a connection pool that's already been torn down, throwing:

java.lang.IllegalStateException: Connection pool shut down

This causes deregistration to fail silently during graceful shutdown, leaving stale instances in the Eureka registry until the lease expires.

History

This overlaps with #4103, which documented the same "new HTTP client per call, never closed" issue. That was addressed in #4258 by caching a shared CloseableHttpClient in DefaultEurekaClientHttpRequestFactorySupplier and making the supplier a Spring DisposableBean to close it on shutdown. That approach was reverted (79a2eb8) after it caused #4275: making the supplier a DisposableBean introduced an independent Spring bean-destroy callback with no ordering guarantee relative to CloudEurekaClient's own @Bean(destroyMethod = "shutdown") - so the shared client could be closed before CloudEurekaClient.shutdown() reached its unregister() call, breaking deregistration outright.

Fix

Same idea as #4258 - share one CloseableHttpClient per supplier instance instead of building one per call - but close it through the Eureka transport lifecycle instead of an independent Spring bean-destroy callback:

  • EurekaClientHttpRequestFactorySupplier gets a new default void close() {} method (backward compatible for existing custom implementations).
  • DefaultEurekaClientHttpRequestFactorySupplier lazily builds and caches a single CloseableHttpClient, reused across all get() calls, and closes it in close(). It does not implement DisposableBean.
  • RestClientTransportClientFactory#shutdown() now calls eurekaClientHttpRequestFactorySupplier.close().

Since TransportClientFactory#shutdown() is invoked synchronously by Netflix's DiscoveryClient.shutdown() - which calls unregister() before eurekaTransport.shutdown() - the pool is now guaranteed to close after the final deregistration request completes, in the same thread, with no race against an unrelated Spring bean-destroy path.

Testing

  • DefaultEurekaClientHttpRequestFactorySupplierTests: verifies the client is reused across get() calls, close() is safe to call before any get() and safe to call twice, and - as an explicit regression guard for When shutting down after 4.1.1, an exception occurs while unregistering #4275 - that this class does not implement DisposableBean.
  • RestClientTransportClientFactoryShutdownTests: verifies shutdown() delegates to supplier.close().
  • Full spring-cloud-netflix-eureka-client test suite passes locally (216/216, excluding one pre-existing Docker-dependent Testcontainers test unrelated to this change).

Signed-off-by: Prahlad Bhakat <prahladbhakat05@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Eureka deregistration fails on graceful shutdown: "Connection pool shut down"

2 participants