Skip to content

Commit

Permalink
Additional HttpClientOptions Configurations (#22248)
Browse files Browse the repository at this point in the history
Additional HttpClientOptions Configurations
  • Loading branch information
alzimmermsft authored Jun 15, 2021
1 parent 84e9238 commit f3fac45
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.azure.core.http.HttpClient;
import com.azure.core.http.HttpClientProvider;
import com.azure.core.util.HttpClientOptions;
import reactor.netty.resources.ConnectionProvider;

/**
* An {@link HttpClientProvider} that provides an implementation of HttpClient based on Netty.
Expand All @@ -27,6 +28,18 @@ public HttpClient createInstance(HttpClientOptions clientOptions) {
.writeTimeout(clientOptions.getWriteTimeout())
.responseTimeout(clientOptions.getResponseTimeout())
.readTimeout(clientOptions.getReadTimeout());

int maximumConnectionPoolSize = clientOptions.getMaximumConnectionPoolSize();

ConnectionProvider.Builder connectionProviderBuilder = ConnectionProvider.builder("azure-sdk");
connectionProviderBuilder.maxIdleTime(clientOptions.getConnectionIdleTimeout());

// Only configure the maximum connections if it has been set in the options.
if (maximumConnectionPoolSize > 0) {
connectionProviderBuilder.maxConnections(maximumConnectionPoolSize);
}

builder = builder.connectionProvider(connectionProviderBuilder.build());
}

return builder.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
package com.azure.core.http.okhttp;

import com.azure.core.http.HttpClient;
import com.azure.core.util.HttpClientOptions;
import com.azure.core.http.HttpClientProvider;
import com.azure.core.util.HttpClientOptions;
import okhttp3.ConnectionPool;

import java.util.concurrent.TimeUnit;

/**
* An {@link HttpClientProvider} that provides an implementation of HttpClient based on OkHttp.
Expand All @@ -26,6 +29,15 @@ public HttpClient createInstance(HttpClientOptions clientOptions) {
.configuration(clientOptions.getConfiguration())
.writeTimeout(clientOptions.getWriteTimeout())
.readTimeout(clientOptions.getReadTimeout());

int maximumConnectionPoolSize = (clientOptions.getMaximumConnectionPoolSize() == 0)
? 5 // By default OkHttp uses a maximum idle connection count of 5.
: clientOptions.getMaximumConnectionPoolSize();

ConnectionPool connectionPool = new ConnectionPool(maximumConnectionPoolSize,
clientOptions.getConnectionIdleTimeout().toMillis(), TimeUnit.MILLISECONDS);

builder = builder.connectionPool(connectionPool);
}

return builder.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,30 @@
import com.azure.core.annotation.Fluent;
import com.azure.core.http.HttpClient;
import com.azure.core.http.ProxyOptions;
import com.azure.core.util.logging.ClientLogger;

import java.time.Duration;

/**
* General configuration options for {@link HttpClient HttpClients}.
* <p>
* {@link HttpClient} implementations may not support all configuration options in this class.
*/
@Fluent
public final class HttpClientOptions extends ClientOptions {
private static final Duration MINIMUM_TIMEOUT = Duration.ofMillis(1);
private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(60);
private static final Duration NO_TIMEOUT = Duration.ZERO;

private final ClientLogger logger = new ClientLogger(HttpClientOptions.class);

private ProxyOptions proxyOptions;
private Configuration configuration;
private Duration writeTimeout;
private Duration responseTimeout;
private Duration readTimeout;
private Integer maximumConnectionPoolSize;
private Duration connectionIdleTimeout;

@Override
public HttpClientOptions setApplicationId(String applicationId) {
Expand Down Expand Up @@ -88,6 +95,8 @@ public Configuration getConfiguration() {
* If {@code writeTimeout} is {@code null} a 60 second timeout will be used, if it is a {@link Duration} less than
* or equal to zero then no write timeout will be applied. When applying the timeout the greater of one millisecond
* and the value of {@code writeTimeout} will be used.
* <p>
* By default the write timeout is 60 seconds.
*
* @param writeTimeout Write operation timeout duration.
* @return The updated HttpClientOptions object.
Expand All @@ -99,6 +108,8 @@ public HttpClientOptions setWriteTimeout(Duration writeTimeout) {

/**
* Gets the write timeout for a request to be sent.
* <p>
* By default the write timeout is 60 seconds.
*
* @return The write timeout of a request to be sent.
*/
Expand All @@ -115,6 +126,8 @@ public Duration getWriteTimeout() {
* If {@code responseTimeout} is {@code null} a 60 second timeout will be used, if it is a {@link Duration} less
* than or equal to zero then no timeout will be applied to the response. When applying the timeout the greater of
* one millisecond and the value of {@code responseTimeout} will be used.
* <p>
* By default the response timeout is 60 seconds.
*
* @param responseTimeout Response timeout duration.
* @return The updated HttpClientOptions object.
Expand All @@ -133,6 +146,8 @@ public HttpClientOptions responseTimeout(Duration responseTimeout) {
* If {@code responseTimeout} is {@code null} a 60 second timeout will be used, if it is a {@link Duration} less
* than or equal to zero then no timeout will be applied to the response. When applying the timeout the greater of
* one millisecond and the value of {@code responseTimeout} will be used.
* <p>
* By default the response timeout is 60 seconds.
*
* @param responseTimeout Response timeout duration.
* @return The updated HttpClientOptions object.
Expand All @@ -144,6 +159,8 @@ public HttpClientOptions setResponseTimeout(Duration responseTimeout) {

/**
* Gets the response timeout duration used when waiting for a server to reply.
* <p>
* By default the response timeout is 60 seconds.
*
* @return The response timeout duration.
*/
Expand All @@ -161,6 +178,8 @@ public Duration getResponseTimeout() {
* If {@code readTimeout} is {@code null} a 60 second timeout will be used, if it is a {@link Duration} less than or
* equal to zero then no timeout period will be applied to response read. When applying the timeout the greater of
* one millisecond and the value of {@code readTimeout} will be used.
* <p>
* By default the read timeout is 60 seconds.
*
* @param readTimeout Read timeout duration.
* @return The updated HttpClientOptions object.
Expand All @@ -180,6 +199,8 @@ public HttpClientOptions readTimeout(Duration readTimeout) {
* If {@code readTimeout} is {@code null} a 60 second timeout will be used, if it is a {@link Duration} less than or
* equal to zero then no timeout period will be applied to response read. When applying the timeout the greater of
* one millisecond and the value of {@code readTimeout} will be used.
* <p>
* By default the read timeout is 60 seconds.
*
* @param readTimeout Read timeout duration.
* @return The updated HttpClientOptions object.
Expand All @@ -191,13 +212,96 @@ public HttpClientOptions setReadTimeout(Duration readTimeout) {

/**
* Gets the read timeout duration used when reading the server response.
* <p>
* By default the read timeout is 60 seconds.
*
* @return The read timeout duration.
*/
public Duration getReadTimeout() {
return getTimeout(readTimeout);
}

/**
* Sets the maximum connection pool size used by the underlying HTTP client.
* <p>
* Modifying the maximum connection pool size may have effects on the performance of an application. Increasing the
* maximum connection pool will result in more connections being available for an application but may result in more
* contention for network resources. It is recommended to perform performance analysis on different maximum
* connection pool sizes to find the right configuration for an application.
* <p>
* This maximum connection pool size is not a global configuration but an instance level configuration for each
* {@link HttpClient} created using this {@link HttpClientOptions}.
* <p>
* By default the maximum connection pool size is determined by the underlying HTTP client. Setting the maximum
* connection pool size to null resets the configuration to use the default determined by the underlying HTTP
* client.
*
* @param maximumConnectionPoolSize The maximum connection pool size.
* @return The updated HttpClientOptions object.
* @throws IllegalArgumentException If {@code maximumConnectionPoolSize} is not null and is less than {@code 1}.
*/
public HttpClientOptions setMaximumConnectionPoolSize(Integer maximumConnectionPoolSize) {
if (maximumConnectionPoolSize != null && maximumConnectionPoolSize <= 0) {
throw logger.logExceptionAsError(
new IllegalArgumentException("'maximumConnectionPoolSize' cannot be less than 1."));
}

this.maximumConnectionPoolSize = maximumConnectionPoolSize;
return this;
}

/**
* Gets the maximum connection pool size used by the underlying HTTP client.
* <p>
* Modifying the maximum connection pool size may have effects on the performance of an application. Increasing the
* maximum connection pool will result in more connections being available for an application but may result in more
* contention for network resources. It is recommended to perform performance analysis on different maximum
* connection pool sizes to find the right configuration for an application.
* <p>
* This maximum connection pool size is not a global configuration but an instance level configuration for each
* {@link HttpClient} created using this {@link HttpClientOptions}.
* <p>
* By default the maximum connection pool size is determined by the underlying HTTP client. Setting the maximum
* connection pool size to null resets the configuration to use the default determined by the underlying HTTP
* client.
*
* @return The maximum connection pool size.
*/
public Integer getMaximumConnectionPoolSize() {
return maximumConnectionPoolSize;
}

/**
* Sets the duration of time before an idle connection.
* <p>
* The connection idle timeout begins once the connection has completed its last network request. Every time the
* connection is used the idle timeout will reset.
* <p>
* If {@code connectionIdleTimeout} is {@code null} a 60 second timeout will be used, if it is a {@link Duration}
* less than or equal to zero then no timeout period will be applied. When applying the timeout the greater of one
* millisecond and the value of {@code connectionIdleTimeout} will be used.
* <p>
* By default the connection idle timeout is 60 seconds.
*
* @param connectionIdleTimeout The connection idle timeout duration.
* @return The updated HttpClientOptions object.
*/
public HttpClientOptions setConnectionIdleTimeout(Duration connectionIdleTimeout) {
this.connectionIdleTimeout = connectionIdleTimeout;
return this;
}

/**
* Gets the duration of time before an idle connection is closed.
* <p>
* By default the connection idle timeout is 60 seconds.
*
* @return The connection idle timeout duration.
*/
public Duration getConnectionIdleTimeout() {
return getTimeout(connectionIdleTimeout);
}

private static Duration getTimeout(Duration timeout) {
// Timeout is null, use the 60 second default.
if (timeout == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.core.util;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.time.Duration;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

/**
* Tests {@link HttpClientOptions}.
*/
public class HttpClientOptionsTests {
@ParameterizedTest
@MethodSource("timeoutSupplier")
public void nullTimeoutDefaultsTo60Seconds(BiFunction<HttpClientOptions, Duration, HttpClientOptions> timeoutSetter,
Function<HttpClientOptions, Duration> timeoutGetter) {
HttpClientOptions httpClientOptions = timeoutSetter.apply(new HttpClientOptions(), null);

assertEquals(Duration.ofSeconds(60), timeoutGetter.apply(httpClientOptions));
}

@ParameterizedTest
@MethodSource("timeoutSupplier")
public void negativeTimeoutDefaultsToInfiniteTimeout(
BiFunction<HttpClientOptions, Duration, HttpClientOptions> timeoutSetter,
Function<HttpClientOptions, Duration> timeoutGetter) {
HttpClientOptions httpClientOptions = timeoutSetter.apply(new HttpClientOptions(), Duration.ofSeconds(-1));

assertEquals(Duration.ZERO, timeoutGetter.apply(httpClientOptions));
}

public void zeroTimeoutDefaultsToInfiniteTimeout(
BiFunction<HttpClientOptions, Duration, HttpClientOptions> timeoutSetter,
Function<HttpClientOptions, Duration> timeoutGetter) {
HttpClientOptions httpClientOptions = timeoutSetter.apply(new HttpClientOptions(), Duration.ZERO);

assertEquals(Duration.ZERO, timeoutGetter.apply(httpClientOptions));
}

@ParameterizedTest
@MethodSource("timeoutSupplier")
public void smallTimeoutDefaultsToOneMillisecond(
BiFunction<HttpClientOptions, Duration, HttpClientOptions> timeoutSetter,
Function<HttpClientOptions, Duration> timeoutGetter) {
HttpClientOptions httpClientOptions = timeoutSetter.apply(new HttpClientOptions(), Duration.ofNanos(1));

assertEquals(Duration.ofMillis(1), timeoutGetter.apply(httpClientOptions));
}

@ParameterizedTest
@MethodSource("timeoutSupplier")
public void timeoutReturnsAsIs(BiFunction<HttpClientOptions, Duration, HttpClientOptions> timeoutSetter,
Function<HttpClientOptions, Duration> timeoutGetter) {
HttpClientOptions httpClientOptions = timeoutSetter.apply(new HttpClientOptions(), Duration.ofMinutes(5));

assertEquals(Duration.ofMinutes(5), timeoutGetter.apply(httpClientOptions));
}

private static Stream<Arguments> timeoutSupplier() {
BiFunction<HttpClientOptions, Duration, HttpClientOptions> setWriteTimeout = HttpClientOptions::setWriteTimeout;
Function<HttpClientOptions, Duration> getWriteTimeout = HttpClientOptions::getWriteTimeout;

BiFunction<HttpClientOptions, Duration, HttpClientOptions> responseTimeout = HttpClientOptions::responseTimeout;
BiFunction<HttpClientOptions, Duration, HttpClientOptions> setResponseTimeout =
HttpClientOptions::setResponseTimeout;
Function<HttpClientOptions, Duration> getResponseTimeout = HttpClientOptions::getResponseTimeout;

BiFunction<HttpClientOptions, Duration, HttpClientOptions> readTimeout = HttpClientOptions::readTimeout;
BiFunction<HttpClientOptions, Duration, HttpClientOptions> setReadTimeout = HttpClientOptions::setReadTimeout;
Function<HttpClientOptions, Duration> getReadTimeout = HttpClientOptions::getReadTimeout;

BiFunction<HttpClientOptions, Duration, HttpClientOptions> setConnectionIdleTimeout =
HttpClientOptions::setConnectionIdleTimeout;
Function<HttpClientOptions, Duration> getConnectionIdleTimeout = HttpClientOptions::getConnectionIdleTimeout;

return Stream.of(
Arguments.of(setWriteTimeout, getWriteTimeout),

Arguments.of(responseTimeout, getResponseTimeout),
Arguments.of(setResponseTimeout, getResponseTimeout),

Arguments.of(readTimeout, getReadTimeout),
Arguments.of(setReadTimeout, getReadTimeout),

Arguments.of(setConnectionIdleTimeout, getConnectionIdleTimeout)
);
}

@Test
public void nullMaximumConnectionPoolSizeRemainsNull() {
HttpClientOptions httpClientOptions = new HttpClientOptions().setMaximumConnectionPoolSize(null);
assertNull(httpClientOptions.getMaximumConnectionPoolSize());
}

@Test
public void maximumConnectionPoolSizeReturnsAsIs() {
HttpClientOptions httpClientOptions = new HttpClientOptions().setMaximumConnectionPoolSize(50);
assertEquals(50, httpClientOptions.getMaximumConnectionPoolSize());
}

@Test
public void zeroOrNegativeMaximumConnectionPoolSizeThrows() {
assertThrows(IllegalArgumentException.class, () -> new HttpClientOptions().setMaximumConnectionPoolSize(0));
assertThrows(IllegalArgumentException.class, () -> new HttpClientOptions().setMaximumConnectionPoolSize(-1));
}
}

0 comments on commit f3fac45

Please sign in to comment.