Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ReactorClientHttpConnector creates new HttpClient for every request #33093

Closed
kzander91 opened this issue Jun 25, 2024 · 4 comments
Closed

ReactorClientHttpConnector creates new HttpClient for every request #33093

kzander91 opened this issue Jun 25, 2024 · 4 comments
Assignees
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: regression A bug that is also a regression
Milestone

Comments

@kzander91
Copy link
Contributor

kzander91 commented Jun 25, 2024

In 6.1.9, since commit 7785f94 (related issue: #32945), this.httpClient is no longer assigned. The assignment should probably have happened here:

if (httpClient == null) {
Assert.state(this.resourceFactory != null && this.mapper != null, "Illegal configuration");
httpClient = createHttpClient(this.resourceFactory, this.mapper);
}

As a consequence, a new HttpClient instance is created for every request. Apart from the possible performance impact, this is causing a memory leak in my application: I'm using client certificate authentication configured through Boot's SslBundle support. This causes the creation of new SslContext instances alongside each new HttpClient in org.springframework.boot.autoconfigure.web.reactive.function.client.ReactorClientHttpConnectorFactory.
Then, because Netty's SslContext implementations don't implement hashCode(), Reactor Netty's connection pooling doesn't work properly, causing new HTTP connection pools to be created for every request (config.channelHash() returns distinct values for every request https://github.com/reactor/reactor-netty/blob/v1.1.20/reactor-netty-core/src/main/java/reactor/netty/resources/PooledConnectionProvider.java#L127-L133).
All these connection pools are then never disposed of (this is Reactor Netty's default configuration I believe), eventually causing an OOME.


TL;DR

  • Reactor Netty creates a new HTTP connection pool for every HTTP request, because
  • it includes the hash code of Netty's SslContext in the pool key, because
  • an SslContext has been configured by Spring Boot's SslBundle feature for every request, because
  • Spring Framework creates new HttpClient instances for every request.

The leak itself may be fixed in Netty (should SslContext implementations have stable hash codes?), but it was surfaced by the commit in Spring Framework.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Jun 25, 2024
@kzander91
Copy link
Contributor Author

kzander91 commented Jun 25, 2024

Further insight: It looks like this happens to all ReactorClientHttpConnector instances that are not Spring Beans and that are created through this constructor at a time when resourceFactory.isRunning() returns false:

public ReactorClientHttpConnector(ReactorResourceFactory resourceFactory, Function<HttpClient, HttpClient> mapper) {
this.resourceFactory = resourceFactory;
this.mapper = mapper;
if (resourceFactory.isRunning()) {
this.httpClient = createHttpClient(resourceFactory, mapper);
}
}

For those instances, the Lifecycle.start() method isn't called, and the httpClient stays null.

@jhoeller jhoeller added in: web Issues in web modules (web, webmvc, webflux, websocket) type: regression A bug that is also a regression and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Jun 25, 2024
@jhoeller jhoeller self-assigned this Jun 25, 2024
@jhoeller jhoeller added this to the 6.1.11 milestone Jun 25, 2024
@jhoeller
Copy link
Contributor

jhoeller commented Jun 25, 2024

Where does the ReactorResourceFactory instance come from in your scenario? It seems to not have been initialized/started at the time of ReactorClientHttpConnector construction yet, so at which point will it receive an afterPropertiesSet() or start() call then? Is the ReactorResourceFactory possibly a Spring bean but ReactorClientHttpConnector is not? If this is the case, we could simply retain the HttpClient instance in connect if the ReactorResourceFactory has been started in the meantime.

@kzander91
Copy link
Contributor Author

kzander91 commented Jun 25, 2024

@jhoeller I'm doing this:

@Bean
WebClient apiV1WebClient(WebClient.Builder builder, WebClientSsl ssl) {
    return builder
            .apply(ssl.fromBundle("client-auth"))
            .build();
}

The ReactorResourceFactory ultimately comes from org.springframework.boot.autoconfigure.reactor.netty.ReactorNettyConfigurations.ReactorResourceFactoryConfiguration. So yes, it is a bean, but the ReactorClientHttpConnector is not. It is created and passed into the web client builder in the apply() call, see https://github.com/spring-projects/spring-boot/blob/586499db56afc755a1e5e623afc5bb636c601562/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/AutoConfiguredWebClientSsl.java#L48-L53.

@jhoeller
Copy link
Contributor

Alright, thanks for the deep dive! I'll fix this for 6.1.11 as suggested above then since it matches the scenario I had in mind there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: regression A bug that is also a regression
Projects
None yet
Development

No branches or pull requests

3 participants