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

Support per-call response timeout in all HttpClient implementations #40017

Merged
merged 12 commits into from
May 10, 2024
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.azure.core.http.jdk.httpclient.implementation.InputStreamTimeoutResponseSubscriber;
import com.azure.core.http.jdk.httpclient.implementation.JdkHttpResponseAsync;
import com.azure.core.http.jdk.httpclient.implementation.JdkHttpResponseSync;
import com.azure.core.implementation.util.HttpUtils;
import com.azure.core.util.Context;
import com.azure.core.util.logging.ClientLogger;
import reactor.core.publisher.Mono;
Expand All @@ -31,8 +32,6 @@
*/
class JdkHttpClient implements HttpClient {
private static final ClientLogger LOGGER = new ClientLogger(JdkHttpClient.class);
private static final String AZURE_EAGERLY_READ_RESPONSE = "azure-eagerly-read-response";
private static final String AZURE_IGNORE_RESPONSE_BODY = "azure-ignore-response-body";

private final java.net.http.HttpClient jdkHttpClient;

Expand Down Expand Up @@ -79,8 +78,8 @@ public Mono<HttpResponse> send(HttpRequest request) {

@Override
public Mono<HttpResponse> send(HttpRequest request, Context context) {
boolean eagerlyReadResponse = (boolean) context.getData(AZURE_EAGERLY_READ_RESPONSE).orElse(false);
boolean ignoreResponseBody = (boolean) context.getData(AZURE_IGNORE_RESPONSE_BODY).orElse(false);
boolean eagerlyReadResponse = (boolean) context.getData(HttpUtils.AZURE_EAGERLY_READ_RESPONSE).orElse(false);
boolean ignoreResponseBody = (boolean) context.getData(HttpUtils.AZURE_IGNORE_RESPONSE_BODY).orElse(false);

Mono<java.net.http.HttpRequest> jdkRequestMono = Mono.fromCallable(() -> toJdkHttpRequest(request, context));

Expand Down Expand Up @@ -111,8 +110,8 @@ public Mono<HttpResponse> send(HttpRequest request, Context context) {

@Override
public HttpResponse sendSync(HttpRequest request, Context context) {
boolean eagerlyReadResponse = (boolean) context.getData(AZURE_EAGERLY_READ_RESPONSE).orElse(false);
boolean ignoreResponseBody = (boolean) context.getData(AZURE_IGNORE_RESPONSE_BODY).orElse(false);
boolean eagerlyReadResponse = (boolean) context.getData(HttpUtils.AZURE_EAGERLY_READ_RESPONSE).orElse(false);
boolean ignoreResponseBody = (boolean) context.getData(HttpUtils.AZURE_IGNORE_RESPONSE_BODY).orElse(false);

java.net.http.HttpRequest jdkRequest = toJdkHttpRequest(request, context);
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@
import java.util.Set;
import java.util.concurrent.Executor;

import static com.azure.core.util.Configuration.PROPERTY_AZURE_REQUEST_CONNECT_TIMEOUT;
import static com.azure.core.util.Configuration.PROPERTY_AZURE_REQUEST_READ_TIMEOUT;
import static com.azure.core.util.Configuration.PROPERTY_AZURE_REQUEST_RESPONSE_TIMEOUT;
import static com.azure.core.util.Configuration.PROPERTY_AZURE_REQUEST_WRITE_TIMEOUT;
import static com.azure.core.util.CoreUtils.getDefaultTimeoutFromEnvironment;
import static com.azure.core.implementation.util.HttpUtils.getDefaultConnectTimeout;
import static com.azure.core.implementation.util.HttpUtils.getDefaultReadTimeout;
import static com.azure.core.implementation.util.HttpUtils.getDefaultResponseTimeout;
import static com.azure.core.implementation.util.HttpUtils.getDefaultWriteTimeout;
import static com.azure.core.implementation.util.HttpUtils.getTimeout;

/**
* Builder to configure and build an instance of the azure-core {@link HttpClient} type using the JDK HttpClient APIs,
Expand All @@ -38,12 +38,6 @@
public class JdkHttpClientBuilder {
private static final ClientLogger LOGGER = new ClientLogger(JdkHttpClientBuilder.class);

private static final Duration MINIMUM_TIMEOUT = Duration.ofMillis(1);
private static final Duration DEFAULT_CONNECTION_TIMEOUT;
private static final Duration DEFAULT_WRITE_TIMEOUT;
private static final Duration DEFAULT_RESPONSE_TIMEOUT;
private static final Duration DEFAULT_READ_TIMEOUT;

private static final String JAVA_HOME = System.getProperty("java.home");
private static final String JDK_HTTPCLIENT_ALLOW_RESTRICTED_HEADERS = "jdk.httpclient.allowRestrictedHeaders";

Expand All @@ -57,17 +51,6 @@ public class JdkHttpClientBuilder {
static final Set<String> DEFAULT_RESTRICTED_HEADERS;

static {
Configuration configuration = Configuration.getGlobalConfiguration();

DEFAULT_CONNECTION_TIMEOUT = getDefaultTimeoutFromEnvironment(configuration,
PROPERTY_AZURE_REQUEST_CONNECT_TIMEOUT, Duration.ofSeconds(10), LOGGER);
DEFAULT_WRITE_TIMEOUT = getDefaultTimeoutFromEnvironment(configuration, PROPERTY_AZURE_REQUEST_WRITE_TIMEOUT,
Duration.ofSeconds(60), LOGGER);
DEFAULT_RESPONSE_TIMEOUT = getDefaultTimeoutFromEnvironment(configuration,
PROPERTY_AZURE_REQUEST_RESPONSE_TIMEOUT, Duration.ofSeconds(60), LOGGER);
DEFAULT_READ_TIMEOUT = getDefaultTimeoutFromEnvironment(configuration, PROPERTY_AZURE_REQUEST_READ_TIMEOUT,
Duration.ofSeconds(60), LOGGER);

DEFAULT_RESTRICTED_HEADERS = Set.of("connection", "content-length", "expect", "host", "upgrade");
}

Expand Down Expand Up @@ -248,11 +231,11 @@ public HttpClient build() {
// Azure JDK http client supports HTTP 1.1 by default.
httpClientBuilder.version(java.net.http.HttpClient.Version.HTTP_1_1);

httpClientBuilder = httpClientBuilder.connectTimeout(getTimeout(connectionTimeout, DEFAULT_CONNECTION_TIMEOUT));
httpClientBuilder = httpClientBuilder.connectTimeout(getTimeout(connectionTimeout, getDefaultConnectTimeout()));

Duration writeTimeout = getTimeout(this.writeTimeout, DEFAULT_WRITE_TIMEOUT);
Duration responseTimeout = getTimeout(this.responseTimeout, DEFAULT_RESPONSE_TIMEOUT);
Duration readTimeout = getTimeout(this.readTimeout, DEFAULT_READ_TIMEOUT);
Duration writeTimeout = getTimeout(this.writeTimeout, getDefaultWriteTimeout());
Duration responseTimeout = getTimeout(this.responseTimeout, getDefaultResponseTimeout());
Duration readTimeout = getTimeout(this.readTimeout, getDefaultReadTimeout());

Configuration buildConfiguration
= (configuration == null) ? Configuration.getGlobalConfiguration() : configuration;
Expand Down Expand Up @@ -331,12 +314,4 @@ protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(this.userName, password.toCharArray());
}
}

private static Duration getTimeout(Duration configuredTimeout, Duration defaultTimeout) {
if (configuredTimeout == null) {
return defaultTimeout;
}

return configuredTimeout.compareTo(MINIMUM_TIMEOUT) < 0 ? MINIMUM_TIMEOUT : configuredTimeout;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import com.azure.core.http.HttpMethod;
import com.azure.core.implementation.util.HttpHeadersAccessHelper;
import com.azure.core.implementation.util.HttpUtils;
import com.azure.core.util.Context;
import com.azure.core.util.Contexts;
import com.azure.core.util.ProgressReporter;
Expand Down Expand Up @@ -51,6 +52,9 @@ public AzureJdkHttpRequest(com.azure.core.http.HttpRequest azureCoreRequest, Con
Set<String> restrictedHeaders, ClientLogger logger, Duration writeTimeout, Duration responseTimeout) {
HttpMethod method = azureCoreRequest.getHttpMethod();
ProgressReporter progressReporter = Contexts.with(context).getHttpRequestProgressReporter();
responseTimeout = (Duration) context.getData(HttpUtils.AZURE_RESPONSE_TIMEOUT)
.filter(timeoutDuration -> timeoutDuration instanceof Duration)
.orElse(responseTimeout);

this.method = method.toString();
this.bodyPublisher = (method == HttpMethod.GET || method == HttpMethod.HEAD)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public final class JdkHttpClientLocalTestServer {
private static volatile LocalTestServer proxyServer;
private static final Semaphore PROXY_SERVER_SEMAPHORE = new Semaphore(1);

public static final String TIMEOUT = "/timeout";

public static final byte[] SHORT_BODY = "hi there".getBytes(StandardCharsets.UTF_8);
public static final byte[] LONG_BODY = createLongBody();

Expand Down Expand Up @@ -104,6 +106,16 @@ private static LocalTestServer initializeServer() {
resp.getHttpOutput().write(SHORT_BODY, 5, 3);
resp.getHttpOutput().flush();
resp.getHttpOutput().complete(Callback.NOOP);
} else if (get && TIMEOUT.equals(path)) {
try {
Thread.sleep(5000);
resp.setStatus(200);
resp.getHttpOutput().write(SHORT_BODY);
resp.getHttpOutput().flush();
resp.getHttpOutput().complete(Callback.NOOP);
} catch (InterruptedException e) {
throw new ServletException(e);
}
} else {
throw new ServletException("Unexpected request: " + req.getMethod() + " " + path);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.azure.core.http.HttpMethod;
import com.azure.core.http.HttpRequest;
import com.azure.core.http.HttpResponse;
import com.azure.core.implementation.util.HttpUtils;
import com.azure.core.util.BinaryData;
import com.azure.core.util.Context;
import com.azure.core.util.Contexts;
Expand Down Expand Up @@ -55,6 +56,7 @@

import static com.azure.core.http.jdk.httpclient.JdkHttpClientLocalTestServer.LONG_BODY;
import static com.azure.core.http.jdk.httpclient.JdkHttpClientLocalTestServer.SHORT_BODY;
import static com.azure.core.http.jdk.httpclient.JdkHttpClientLocalTestServer.TIMEOUT;
import static com.azure.core.test.utils.TestUtils.assertArraysEqual;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
Expand Down Expand Up @@ -469,6 +471,46 @@ public void slowEagerReadingTimesOutAsync() {
.verify(Duration.ofSeconds(5));
}

@Test
public void perCallTimeout() {
HttpClient client = new JdkHttpClientBuilder().responseTimeout(Duration.ofSeconds(10)).build();

HttpRequest request = new HttpRequest(HttpMethod.GET, url(TIMEOUT));

// Verify a smaller timeout sent through Context times out the request.
StepVerifier.create(client.send(request, new Context(HttpUtils.AZURE_RESPONSE_TIMEOUT, Duration.ofSeconds(1))))
.expectErrorMatches(e -> e instanceof HttpTimeoutException)
.verify();

// Then verify not setting a timeout through Context does not time out the request.
StepVerifier.create(client.send(request)
.flatMap(response -> Mono.zip(FluxUtil.collectBytesInByteBufferStream(response.getBody()),
Mono.just(response.getStatusCode()))))
.assertNext(tuple -> {
assertArraysEqual(SHORT_BODY, tuple.getT1());
assertEquals(200, tuple.getT2());
})
.verifyComplete();
}

@Test
public void perCallTimeoutSync() {
HttpClient client = new JdkHttpClientBuilder().responseTimeout(Duration.ofSeconds(10)).build();

HttpRequest request = new HttpRequest(HttpMethod.GET, url(TIMEOUT));

// Verify a smaller timeout sent through Context times out the request.
RuntimeException ex = assertThrows(RuntimeException.class,
() -> client.sendSync(request, new Context(HttpUtils.AZURE_RESPONSE_TIMEOUT, Duration.ofSeconds(1))));
assertInstanceOf(HttpTimeoutException.class, ex.getCause());

// Then verify not setting a timeout through Context does not time out the request.
try (HttpResponse response = client.sendSync(request, Context.NONE)) {
assertEquals(200, response.getStatusCode());
assertArraysEqual(SHORT_BODY, response.getBodyAsBinaryData().toBytes());
}
}

private static Mono<HttpResponse> getResponse(String path) {
HttpClient client = new JdkHttpClientBuilder().build();
return doRequest(client, path);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import com.azure.core.implementation.util.BinaryDataHelper;
import com.azure.core.implementation.util.ByteArrayContent;
import com.azure.core.implementation.util.FileContent;
import com.azure.core.implementation.util.HttpUtils;
import com.azure.core.implementation.util.InputStreamContent;
import com.azure.core.implementation.util.SerializableContent;
import com.azure.core.implementation.util.StringContent;
Expand Down Expand Up @@ -94,11 +95,6 @@ class NettyAsyncHttpClient implements HttpClient {
private static final ClientLogger LOGGER = new ClientLogger(NettyAsyncHttpClient.class);
private static final byte[] EMPTY_BYTES = new byte[0];

private static final String AZURE_EAGERLY_READ_RESPONSE = "azure-eagerly-read-response";
private static final String AZURE_IGNORE_RESPONSE_BODY = "azure-ignore-response-body";
private static final String AZURE_RESPONSE_TIMEOUT = "azure-response-timeout";
private static final String AZURE_EAGERLY_CONVERT_HEADERS = "azure-eagerly-convert-headers";

final boolean disableBufferCopy;

final boolean addProxyHandler;
Expand Down Expand Up @@ -132,10 +128,11 @@ public Mono<HttpResponse> send(HttpRequest request, Context context) {
Objects.requireNonNull(request.getUrl(), "'request.getUrl()' cannot be null.");
Objects.requireNonNull(request.getUrl().getProtocol(), "'request.getUrl().getProtocol()' cannot be null.");

boolean eagerlyReadResponse = (boolean) context.getData(AZURE_EAGERLY_READ_RESPONSE).orElse(false);
boolean ignoreResponseBody = (boolean) context.getData(AZURE_IGNORE_RESPONSE_BODY).orElse(false);
boolean headersEagerlyConverted = (boolean) context.getData(AZURE_EAGERLY_CONVERT_HEADERS).orElse(false);
Long responseTimeout = context.getData(AZURE_RESPONSE_TIMEOUT)
boolean eagerlyReadResponse = (boolean) context.getData(HttpUtils.AZURE_EAGERLY_READ_RESPONSE).orElse(false);
boolean ignoreResponseBody = (boolean) context.getData(HttpUtils.AZURE_IGNORE_RESPONSE_BODY).orElse(false);
boolean headersEagerlyConverted
= (boolean) context.getData(HttpUtils.AZURE_EAGERLY_CONVERT_HEADERS).orElse(false);
Long responseTimeout = context.getData(HttpUtils.AZURE_RESPONSE_TIMEOUT)
.filter(timeoutDuration -> timeoutDuration instanceof Duration)
.map(timeoutDuration -> ((Duration) timeoutDuration).toMillis())
.orElse(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,14 @@
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;

import static com.azure.core.util.Configuration.PROPERTY_AZURE_REQUEST_CONNECT_TIMEOUT;
import static com.azure.core.util.Configuration.PROPERTY_AZURE_REQUEST_READ_TIMEOUT;
import static com.azure.core.util.Configuration.PROPERTY_AZURE_REQUEST_RESPONSE_TIMEOUT;
import static com.azure.core.util.Configuration.PROPERTY_AZURE_REQUEST_WRITE_TIMEOUT;
import static com.azure.core.util.CoreUtils.getDefaultTimeoutFromEnvironment;
import static com.azure.core.implementation.util.HttpUtils.getDefaultConnectTimeout;
import static com.azure.core.implementation.util.HttpUtils.getDefaultReadTimeout;
import static com.azure.core.implementation.util.HttpUtils.getDefaultResponseTimeout;
import static com.azure.core.implementation.util.HttpUtils.getDefaultWriteTimeout;
import static com.azure.core.implementation.util.HttpUtils.getTimeout;

/**
* <p>
Expand Down Expand Up @@ -112,27 +111,10 @@
* @see NettyAsyncHttpClient
*/
public class NettyAsyncHttpClientBuilder {
private static final long MINIMUM_TIMEOUT = TimeUnit.MILLISECONDS.toMillis(1);
private static final long DEFAULT_CONNECT_TIMEOUT;
private static final long DEFAULT_WRITE_TIMEOUT;
private static final long DEFAULT_RESPONSE_TIMEOUT;
private static final long DEFAULT_READ_TIMEOUT;

// NettyAsyncHttpClientBuilder may be instantiated many times, use a static logger.
private static final ClientLogger LOGGER = new ClientLogger(NettyAsyncHttpClientBuilder.class);

static {
Configuration configuration = Configuration.getGlobalConfiguration();

DEFAULT_CONNECT_TIMEOUT = getDefaultTimeoutFromEnvironment(configuration,
PROPERTY_AZURE_REQUEST_CONNECT_TIMEOUT, Duration.ofSeconds(10), LOGGER).toMillis();
DEFAULT_WRITE_TIMEOUT = getDefaultTimeoutFromEnvironment(configuration, PROPERTY_AZURE_REQUEST_WRITE_TIMEOUT,
Duration.ofSeconds(60), LOGGER).toMillis();
DEFAULT_RESPONSE_TIMEOUT = getDefaultTimeoutFromEnvironment(configuration,
PROPERTY_AZURE_REQUEST_RESPONSE_TIMEOUT, Duration.ofSeconds(60), LOGGER).toMillis();
DEFAULT_READ_TIMEOUT = getDefaultTimeoutFromEnvironment(configuration, PROPERTY_AZURE_REQUEST_READ_TIMEOUT,
Duration.ofSeconds(60), LOGGER).toMillis();

Utility.validateNettyVersions();
}

Expand Down Expand Up @@ -205,17 +187,17 @@ public com.azure.core.http.HttpClient build() {
addressResolverWasSetByBuilder = true;
}

long writeTimeout = getTimeoutMillis(this.writeTimeout, DEFAULT_WRITE_TIMEOUT);
long responseTimeout = getTimeoutMillis(this.responseTimeout, DEFAULT_RESPONSE_TIMEOUT);
long readTimeout = getTimeoutMillis(this.readTimeout, DEFAULT_READ_TIMEOUT);
long writeTimeout = getTimeout(this.writeTimeout, getDefaultWriteTimeout()).toMillis();
long responseTimeout = getTimeout(this.responseTimeout, getDefaultResponseTimeout()).toMillis();
long readTimeout = getTimeout(this.readTimeout, getDefaultReadTimeout()).toMillis();

// Get the initial HttpResponseDecoderSpec and update it.
// .httpResponseDecoder passes a new HttpResponseDecoderSpec and any existing configuration should be updated
// instead of overwritten.
HttpResponseDecoderSpec initialSpec = nettyHttpClient.configuration().decoder();
nettyHttpClient = nettyHttpClient.port(port)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,
(int) getTimeoutMillis(connectTimeout, DEFAULT_CONNECT_TIMEOUT))
(int) getTimeout(connectTimeout, getDefaultConnectTimeout()).toMillis())
// TODO (alzimmer): What does validating HTTP response headers get us?
.httpResponseDecoder(httpResponseDecoderSpec -> initialSpec.validateHeaders(false))
.doOnRequest(
Expand Down Expand Up @@ -562,27 +544,6 @@ private static ProxyProvider.Proxy toReactorNettyProxyType(ProxyOptions.Type azu
}
}

/*
* Returns the timeout in milliseconds to use based on the passed Duration and default timeout.
*
* If the timeout is {@code null} the default timeout will be used. If the timeout is less than or equal to zero
* no timeout will be used. If the timeout is less than one millisecond a timeout of one millisecond will be used.
*/
static long getTimeoutMillis(Duration configuredTimeout, long defaultTimeout) {
// Timeout is null, use the default timeout.
if (configuredTimeout == null) {
return defaultTimeout;
}

// Timeout is less than or equal to zero, return no timeout.
if (configuredTimeout.isZero() || configuredTimeout.isNegative()) {
return 0;
}

// Return the maximum of the timeout period and the minimum allowed timeout period.
return Math.max(configuredTimeout.toMillis(), MINIMUM_TIMEOUT);
}

private static boolean shouldApplyProxy(SocketAddress socketAddress, Pattern nonProxyHostsPattern) {
if (nonProxyHostsPattern == null) {
return true;
Expand Down
Loading
Loading