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

Client connection limiter #9414

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions docs/src/main/asciidoc/se/webclient.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,55 @@ include::{sourcedir}/se/WebClientSnippets.java[tag=snippet_8,indent=0]
<1> `application.yaml` is a default configuration source loaded when YAML support is on classpath, so we can just use `Config.create()`
<2> Passing the client configuration node

=== Setting connection limits
It is possible to limit connection numbers for specific hosts, proxies or even the total number of connections the client is allowed to create. None of these limits is mandatory to set.

In the examples below we are setting fixed limit implementations, but it is possible to use any implementation of the interface `io.helidon.common.concurrency.limits.Limit`.

Note: Connection limiting is currently supported only for the HTTP1 client connections.

==== Setting connection limit in your code
Below is an example of how to set connection limit to your client programmatically.

[source,java]
----
include::{sourcedir}/se/WebClientSnippets.java[tag=snippet_13,indent=0]
----

<1> Overall connection limit set to 100. The client will not create more active connections than that.
<2> Setting a per-host limit. Every host will have their connection count limited to 5.
<3> This is how we can set limit only to the specific host. This overrides the per-host limit set above. This client will not create more than 2 connections to the host with the name `some-host`.
<4> Disable shared cache so your cache configuration can take effect.

==== Setting connection limit via configuration
It is also possible to set different limits via configuration.

[source,yaml]
.Setting up Http1Client configuration into the `application.yaml` file.
----
client:
share-connection-cache: false
connection-cache-config:
connection-limit:
fixed:
permits: 100
connection-per-host-limit:
fixed:
permits: 5
host-limits:
- host: "some-host"
limit:
fixed:
permits: 2
----
Then, in your application code, load the configuration from that file.

[source,java]
.Http1Client initialization using the `application.yaml` file located on the classpath
----
include::{sourcedir}/se/WebClientSnippets.java[tag=snippet_14,indent=0]
----

== Reference

* link:{webclient-javadoc-base-url}.api/module-summary.html[Helidon Webclient API]
Expand Down
28 changes: 27 additions & 1 deletion docs/src/main/java/io/helidon/docs/se/WebClientSnippets.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,18 @@
*/
package io.helidon.docs.se;

import io.helidon.common.concurrency.limits.FixedLimit;
import io.helidon.common.media.type.MediaTypes;
import io.helidon.config.Config;
import io.helidon.http.Method;
import io.helidon.http.media.MediaSupport;
import io.helidon.webclient.api.ClientResponseTyped;
import io.helidon.webclient.api.HttpClientRequest;
import io.helidon.webclient.api.HttpClientResponse;
import io.helidon.webclient.api.Proxy;
import io.helidon.webclient.api.WebClient;
import io.helidon.webclient.http1.Http1Client;
import io.helidon.webclient.http1.Http1ClientProtocolConfig;
import io.helidon.webclient.http1.Http1ConnectionCacheConfig;
import io.helidon.webclient.metrics.WebClientMetrics;
import io.helidon.webclient.spi.WebClientService;

Expand Down Expand Up @@ -176,4 +178,28 @@ void snippet_12() {
// end::snippet_12[]
}

void snippet_13() {
// tag::snippet_13[]
Http1ConnectionCacheConfig cacheConfig = Http1ConnectionCacheConfig.builder()
.connectionLimit(FixedLimit.builder().permits(100).build()) // <1>
.connectionPerHostLimit(FixedLimit.builder().permits(5).build()) // <2>
.addHostLimit(builder -> builder.host("some-host") // <3>
.limit(FixedLimit.builder().permits(2).build()))
.build();

Http1Client client = Http1Client.builder()
.shareConnectionCache(false) // <4>
.connectionCacheConfig(cacheConfig)
.build();
// end::snippet_13[]
}

void snippet_14() {
// tag::snippet_14[]
Config config = Config.create();

Http1Client client = Http1Client.create(config.get("client"));
// end::snippet_14[]
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,12 @@ public void closeResource() {
if (closed) {
return;
}
try {
this.socket.close();
} catch (IOException e) {
LOGGER.log(TRACE, "Failed to close a client socket", e);
if (this.socket != null) {
try {
this.socket.close();
} catch (IOException e) {
LOGGER.log(TRACE, "Failed to close a client socket", e);
}
}
this.closed = true;
closeConsumer.accept(this);
Expand Down
2 changes: 1 addition & 1 deletion webclient/api/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,5 @@
uses io.helidon.webclient.spi.WebClientServiceProvider;
uses io.helidon.webclient.spi.ProtocolConfigProvider;
uses io.helidon.webclient.spi.HttpClientSpiProvider;

}
14 changes: 14 additions & 0 deletions webclient/http1/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@
<groupId>io.helidon.common</groupId>
<artifactId>helidon-common-context</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.common.concurrency</groupId>
<artifactId>helidon-common-concurrency-limits</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.common.features</groupId>
<artifactId>helidon-common-features-api</artifactId>
Expand Down Expand Up @@ -102,6 +106,16 @@
<artifactId>helidon-common-testing-http-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config-yaml</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, 2023 Oracle and/or its affiliates.
* Copyright (c) 2022, 2024 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -45,7 +45,8 @@ public interface Http1Client extends HttpClient<Http1ClientRequest>, RuntimeType
* @return fluent API builder
*/
static Http1ClientConfig.Builder builder() {
return Http1ClientConfig.builder();
return Http1ClientConfig.builder()
.update(it -> it.from(Http1ClientImpl.globalConfig()));
}

/**
Expand All @@ -65,8 +66,7 @@ static Http1Client create(Http1ClientConfig clientConfig) {
* @return a new client
*/
static Http1Client create(Consumer<Http1ClientConfig.Builder> consumer) {
return Http1ClientConfig.builder()
.update(consumer)
return builder().update(consumer)
.build();
}

Expand All @@ -88,4 +88,15 @@ static Http1Client create() {
static Http1Client create(Config config) {
return create(it -> it.config(config));
}

/**
* Configure the default Http1 client configuration.
* Note: This method needs to be used before Helidon is started to have the full effect.
*
* @param clientConfig global client config
*/
static void configureDefaults(Http1ClientConfig clientConfig) {
Http1ClientImpl.GLOBAL_CONFIG.compareAndSet(null, clientConfig);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
/**
* HTTP/1.1. full webclient configuration.
*/
@Prototype.Configured
@Prototype.Blueprint
interface Http1ClientConfigBlueprint extends HttpClientConfig, Prototype.Factory<Http1Client> {
/**
Expand All @@ -32,4 +33,14 @@ interface Http1ClientConfigBlueprint extends HttpClientConfig, Prototype.Factory
*/
@Option.Default("create()")
Http1ClientProtocolConfig protocolConfig();

/**
* Client connection cache configuration.
*
* @return cache configuration
*/
@Option.Default("create()")
@Option.Configured
Http1ConnectionCacheConfig connectionCacheConfig();

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, 2023 Oracle and/or its affiliates.
* Copyright (c) 2022, 2024 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,6 +16,12 @@

package io.helidon.webclient.http1;

import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;

import io.helidon.common.LazyValue;
import io.helidon.common.config.Config;
import io.helidon.common.config.GlobalConfig;
import io.helidon.http.Method;
import io.helidon.webclient.api.ClientRequest;
import io.helidon.webclient.api.ClientUri;
Expand All @@ -24,6 +30,15 @@
import io.helidon.webclient.spi.HttpClientSpi;

class Http1ClientImpl implements Http1Client, HttpClientSpi {
static final AtomicReference<Http1ClientConfig> GLOBAL_CONFIG = new AtomicReference<>();
private static final LazyValue<Http1ClientConfig> LAZY_GLOBAL_CONFIG = LazyValue.create(() -> {
Config config = GlobalConfig.config();
return Http1ClientConfig.builder()
.servicesDiscoverServices(false)
.config(config.get("client"))
.buildPrototype();
});

private final WebClient webClient;
private final Http1ClientConfig clientConfig;
private final Http1ClientProtocolConfig protocolConfig;
Expand All @@ -38,11 +53,16 @@ class Http1ClientImpl implements Http1Client, HttpClientSpi {
this.connectionCache = Http1ConnectionCache.shared();
this.clientCache = null;
} else {
this.connectionCache = Http1ConnectionCache.create();
this.connectionCache = Http1ConnectionCache.create(clientConfig.connectionCacheConfig());
this.clientCache = connectionCache;
}
}

static Http1ClientConfig globalConfig() {
return Optional.ofNullable(Http1ClientImpl.GLOBAL_CONFIG.get())
.orElseGet(Http1ClientImpl.LAZY_GLOBAL_CONFIG);
}

@Override
public Http1ClientRequest method(Method method) {
ClientUri clientUri = clientConfig.baseUri()
Expand Down
Loading
Loading