Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,35 @@ public interface AsyncHttpClientConfig {

/**
* Return the maximum time an {@link AsyncHttpClient} waits until the response is completed.
* <p>
* By default this bounds each attempt within an exchange rather than the exchange as a whole: a redirect, a
* retry and an auth replay each start it again, so a chain of n hops may run for n times this value. Set
* {@link #isUseAbsoluteRequestDeadline()} to bound the exchange instead.
*
* @return the maximum time an {@link AsyncHttpClient} waits until the response is completed.
*/
Duration getRequestTimeout();

/**
* Whether {@link #getRequestTimeout()} is a deadline for the whole exchange rather than for each attempt
* within it.
* <p>
* A redirect, a retry and an auth replay all continue the same exchange on the same response future, but
* each builds its own timeout state. Anchoring the deadline on that state gives every hop a fresh budget,
* which is why a five-redirect chain can legitimately take six times the configured timeout today. Enabling
* this anchors it on when the exchange was submitted instead, so a later hop gets whatever is left and the
* caller's total wait is bounded by the one value.
* <p>
* Off by default because turning it on shortens exchanges that rely on the per-attempt behaviour. A caller
* working to an end-to-end budget wants it on; {@link Request#getUseAbsoluteRequestDeadline()} sets it for a
* single request.
*
* @return {@code true} to treat the request timeout as a deadline for the whole exchange
*/
default boolean isUseAbsoluteRequestDeadline() {
return false;
}

/**
* Is HTTP redirect enabled
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
import static org.asynchttpclient.config.AsyncHttpClientConfigDefaults.defaultEnabledProtocols;
import static org.asynchttpclient.config.AsyncHttpClientConfigDefaults.defaultExpiredCookieEvictionDelay;
import static org.asynchttpclient.config.AsyncHttpClientConfigDefaults.defaultFailedIpCooldownEnabled;
import static org.asynchttpclient.config.AsyncHttpClientConfigDefaults.defaultUseAbsoluteRequestDeadline;
import static org.asynchttpclient.config.AsyncHttpClientConfigDefaults.defaultFailedIpCooldownPeriod;
import static org.asynchttpclient.config.AsyncHttpClientConfigDefaults.defaultFilterInsecureCipherSuites;
import static org.asynchttpclient.config.AsyncHttpClientConfigDefaults.defaultFollowRedirect;
Expand Down Expand Up @@ -138,6 +139,7 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig {
private final int maxRequestRetry;
private final LoadBalance loadBalance;
private final boolean failedIpCooldownEnabled;
private final boolean useAbsoluteRequestDeadline;
private final Duration failedIpCooldownPeriod;
private final boolean disableUrlEncodingForBoundRequests;
private final boolean useLaxCookieEncoder;
Expand Down Expand Up @@ -243,6 +245,7 @@ private DefaultAsyncHttpClientConfig(// http
int maxRequestRetry,
LoadBalance loadBalance,
boolean failedIpCooldownEnabled,
boolean useAbsoluteRequestDeadline,
Duration failedIpCooldownPeriod,
boolean disableUrlEncodingForBoundRequests,
boolean useLaxCookieEncoder,
Expand Down Expand Up @@ -348,6 +351,7 @@ private DefaultAsyncHttpClientConfig(// http
this.maxRequestRetry = maxRequestRetry;
this.loadBalance = loadBalance;
this.failedIpCooldownEnabled = failedIpCooldownEnabled;
this.useAbsoluteRequestDeadline = useAbsoluteRequestDeadline;
this.failedIpCooldownPeriod = failedIpCooldownPeriod;
this.disableUrlEncodingForBoundRequests = disableUrlEncodingForBoundRequests;
this.useLaxCookieEncoder = useLaxCookieEncoder;
Expand Down Expand Up @@ -518,6 +522,11 @@ public boolean isFailedIpCooldownEnabled() {
return failedIpCooldownEnabled;
}

@Override
public boolean isUseAbsoluteRequestDeadline() {
return useAbsoluteRequestDeadline;
}

@Override
public Duration getFailedIpCooldownPeriod() {
return failedIpCooldownPeriod;
Expand Down Expand Up @@ -937,6 +946,7 @@ public static class Builder {
private int maxRequestRetry = defaultMaxRequestRetry();
private LoadBalance loadBalance = defaultLoadBalance();
private boolean failedIpCooldownEnabled = defaultFailedIpCooldownEnabled();
private boolean useAbsoluteRequestDeadline = defaultUseAbsoluteRequestDeadline();
private Duration failedIpCooldownPeriod = defaultFailedIpCooldownPeriod();
private boolean disableUrlEncodingForBoundRequests = defaultDisableUrlEncodingForBoundRequests();
private boolean useLaxCookieEncoder = defaultUseLaxCookieEncoder();
Expand Down Expand Up @@ -1045,6 +1055,7 @@ public Builder(AsyncHttpClientConfig config) {
maxRequestRetry = config.getMaxRequestRetry();
loadBalance = config.getLoadBalance();
failedIpCooldownEnabled = config.isFailedIpCooldownEnabled();
useAbsoluteRequestDeadline = config.isUseAbsoluteRequestDeadline();
failedIpCooldownPeriod = config.getFailedIpCooldownPeriod();
disableUrlEncodingForBoundRequests = config.isDisableUrlEncodingForBoundRequests();
useLaxCookieEncoder = config.isUseLaxCookieEncoder();
Expand Down Expand Up @@ -1244,6 +1255,17 @@ public Builder setFailedIpCooldownEnabled(boolean failedIpCooldownEnabled) {
return this;
}

/**
* @param useAbsoluteRequestDeadline whether the request timeout is a deadline for the whole exchange
* rather than for each attempt within it; see
* {@link AsyncHttpClientConfig#isUseAbsoluteRequestDeadline()}
* @return this
*/
public Builder setUseAbsoluteRequestDeadline(boolean useAbsoluteRequestDeadline) {
this.useAbsoluteRequestDeadline = useAbsoluteRequestDeadline;
return this;
}

/**
* @param failedIpCooldownPeriod how long a failed IP is deprioritized before it is re-probed;
* {@code null} resets to the default. Must not be negative; use
Expand Down Expand Up @@ -1751,6 +1773,7 @@ public DefaultAsyncHttpClientConfig build() {
maxRequestRetry,
loadBalance,
failedIpCooldownEnabled,
useAbsoluteRequestDeadline,
failedIpCooldownPeriod,
disableUrlEncodingForBoundRequests,
useLaxCookieEncoder,
Expand Down
46 changes: 46 additions & 0 deletions client/src/main/java/org/asynchttpclient/DefaultRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public class DefaultRequest implements Request {
private final @Nullable Realm realm;
private final @Nullable File file;
private final @Nullable Boolean followRedirect;
private final @Nullable Boolean useAbsoluteRequestDeadline;
private final Duration requestTimeout;
private final Duration readTimeout;
private final long rangeOffset;
Expand Down Expand Up @@ -99,6 +100,45 @@ public DefaultRequest(String method,
@Nullable Charset charset,
ChannelPoolPartitioning channelPoolPartitioning,
NameResolver<InetAddress> nameResolver) {
this(method, uri, address, localAddress, headers, cookies, byteData, compositeByteData, stringData,
byteBufferData, byteBufData, streamData, bodyGenerator, formParams, bodyParts, virtualHost,
proxyServer, realm, file, followRedirect, requestTimeout, readTimeout, rangeOffset, charset,
channelPoolPartitioning, nameResolver, null);
}

/**
* @param useAbsoluteRequestDeadline whether {@code requestTimeout} bounds the whole exchange rather than
* each attempt within it, or null to defer to the client config. Trailing
* rather than beside {@code followRedirect} so the original signature
* stays intact for callers that build a request without the builder.
*/
public DefaultRequest(String method,
Uri uri,
@Nullable InetAddress address,
@Nullable InetAddress localAddress,
HttpHeaders headers,
List<Cookie> cookies,
byte @Nullable [] byteData,
@Nullable List<byte[]> compositeByteData,
@Nullable String stringData,
@Nullable ByteBuffer byteBufferData,
@Nullable ByteBuf byteBufData,
@Nullable InputStream streamData,
@Nullable BodyGenerator bodyGenerator,
List<Param> formParams,
List<Part> bodyParts,
@Nullable String virtualHost,
@Nullable ProxyServer proxyServer,
@Nullable Realm realm,
@Nullable File file,
@Nullable Boolean followRedirect,
@Nullable Duration requestTimeout,
@Nullable Duration readTimeout,
long rangeOffset,
@Nullable Charset charset,
ChannelPoolPartitioning channelPoolPartitioning,
NameResolver<InetAddress> nameResolver,
@Nullable Boolean useAbsoluteRequestDeadline) {
this.method = method;
this.uri = uri;
this.address = address;
Expand All @@ -119,6 +159,7 @@ public DefaultRequest(String method,
this.realm = realm;
this.file = file;
this.followRedirect = followRedirect;
this.useAbsoluteRequestDeadline = useAbsoluteRequestDeadline;
this.requestTimeout = requestTimeout == null ? Duration.ZERO : requestTimeout;
this.readTimeout = readTimeout == null ? Duration.ZERO : readTimeout;
this.rangeOffset = rangeOffset;
Expand Down Expand Up @@ -232,6 +273,11 @@ public List<Part> getBodyParts() {
return followRedirect;
}

@Override
public @Nullable Boolean getUseAbsoluteRequestDeadline() {
return useAbsoluteRequestDeadline;
}

@Override
public Duration getRequestTimeout() {
return requestTimeout;
Expand Down
11 changes: 11 additions & 0 deletions client/src/main/java/org/asynchttpclient/Request.java
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,17 @@ public interface Request {
@Nullable
Boolean getFollowRedirect();

/**
* Whether {@link #getRequestTimeout()} is a deadline for the whole exchange rather than for each attempt
* within it. See {@link AsyncHttpClientConfig#isUseAbsoluteRequestDeadline()}.
*
* @return the override, or null to use the config value
*/
@Nullable
default Boolean getUseAbsoluteRequestDeadline() {
return null;
}

/**
* @return the request timeout. Non zero values means "override config value".
*/
Expand Down
17 changes: 16 additions & 1 deletion client/src/main/java/org/asynchttpclient/RequestBuilderBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ public abstract class RequestBuilderBase<T extends RequestBuilderBase<T>> {
protected @Nullable Realm realm;
protected @Nullable File file;
protected @Nullable Boolean followRedirect;
protected @Nullable Boolean useAbsoluteRequestDeadline;
protected @Nullable Duration requestTimeout;
protected @Nullable Duration readTimeout;
protected long rangeOffset;
Expand Down Expand Up @@ -165,6 +166,7 @@ protected RequestBuilderBase(Request prototype, boolean disableUrlEncoding, bool
realm = prototype.getRealm();
file = prototype.getFile();
followRedirect = prototype.getFollowRedirect();
useAbsoluteRequestDeadline = prototype.getUseAbsoluteRequestDeadline();
requestTimeout = prototype.getRequestTimeout();
readTimeout = prototype.getReadTimeout();
rangeOffset = prototype.getRangeOffset();
Expand Down Expand Up @@ -598,6 +600,17 @@ public T setRealm(Realm realm) {
return asDerivedType();
}

/**
* @param useAbsoluteRequestDeadline whether this request's timeout is a deadline for the whole exchange
* rather than for each attempt within it, overriding
* {@link AsyncHttpClientConfig#isUseAbsoluteRequestDeadline()}
* @return {@code this}
*/
public T setUseAbsoluteRequestDeadline(boolean useAbsoluteRequestDeadline) {
this.useAbsoluteRequestDeadline = useAbsoluteRequestDeadline;
return asDerivedType();
}

public T setFollowRedirect(boolean followRedirect) {
this.followRedirect = followRedirect;
return asDerivedType();
Expand Down Expand Up @@ -685,6 +698,7 @@ private RequestBuilderBase<?> executeSignatureCalculator() {
rb.realm = realm;
rb.file = file;
rb.followRedirect = followRedirect;
rb.useAbsoluteRequestDeadline = useAbsoluteRequestDeadline;
rb.requestTimeout = requestTimeout;
rb.rangeOffset = rangeOffset;
rb.charset = charset;
Expand Down Expand Up @@ -755,6 +769,7 @@ public Request build() {
rb.rangeOffset,
rb.charset,
rb.channelPoolPartitioning,
rb.nameResolver);
rb.nameResolver,
rb.useAbsoluteRequestDeadline);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public final class AsyncHttpClientConfigDefaults {
public static final String MAX_REQUEST_RETRY_CONFIG = "maxRequestRetry";
public static final String LOAD_BALANCE_CONFIG = "loadBalance";
public static final String FAILED_IP_COOLDOWN_ENABLED_CONFIG = "failedIpCooldownEnabled";
public static final String USE_ABSOLUTE_REQUEST_DEADLINE_CONFIG = "useAbsoluteRequestDeadline";
public static final String FAILED_IP_COOLDOWN_PERIOD_CONFIG = "failedIpCooldownPeriod";
public static final String DISABLE_URL_ENCODING_FOR_BOUND_REQUESTS_CONFIG = "disableUrlEncodingForBoundRequests";
public static final String USE_LAX_COOKIE_ENCODER_CONFIG = "useLaxCookieEncoder";
Expand Down Expand Up @@ -183,6 +184,10 @@ public static boolean defaultFailedIpCooldownEnabled() {
return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getBoolean(ASYNC_CLIENT_CONFIG_ROOT + FAILED_IP_COOLDOWN_ENABLED_CONFIG);
}

public static boolean defaultUseAbsoluteRequestDeadline() {
return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getBoolean(ASYNC_CLIENT_CONFIG_ROOT + USE_ABSOLUTE_REQUEST_DEADLINE_CONFIG);
}

public static Duration defaultFailedIpCooldownPeriod() {
return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getDuration(ASYNC_CLIENT_CONFIG_ROOT + FAILED_IP_COOLDOWN_PERIOD_CONFIG);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ public final class NettyResponseFuture<V> implements ListenableFuture<V> {
// future no longer takes, which is how a connection through a proxy comes to be offered as a direct one.
// Volatile: the mutators run on the redirect and replay paths while reads happen on other threads.
private volatile Object basePartitionKeyCache;
// Read when a TimeoutsHolder is built, which happens on the caller thread, an event loop or the timer
// thread depending on the path, so it is published rather than plain.
private volatile boolean useAbsoluteRequestDeadline;

public NettyResponseFuture(Request originalRequest,
AsyncHandler<V> asyncHandler,
Expand Down Expand Up @@ -726,6 +729,21 @@ public void acquirePartitionLockLazily(boolean nonBlocking) throws IOException {
}
}

/**
* Whether this exchange's request timeout is a deadline for the exchange as a whole. Resolved once, from the
* request the caller submitted and the client config, and then kept here rather than re-read per hop: a
* redirect rebuilds the request from a hand-picked set of fields, so anything carried only on the request
* would silently revert to the config value partway through the exchange, which is exactly the case this
* setting exists for.
*/
public boolean isUseAbsoluteRequestDeadline() {
return useAbsoluteRequestDeadline;
}

public void setUseAbsoluteRequestDeadline(boolean useAbsoluteRequestDeadline) {
this.useAbsoluteRequestDeadline = useAbsoluteRequestDeadline;
}

public Realm getRealm() {
return realm;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
import static org.asynchttpclient.util.HttpUtils.GZIP_DEFLATE;
import static org.asynchttpclient.util.HttpUtils.GZIP_DEFLATE_HPACK;
import static org.asynchttpclient.util.HttpUtils.hostHeader;
import static org.asynchttpclient.util.HttpUtils.useAbsoluteRequestDeadline;
import static org.asynchttpclient.util.MiscUtils.getCause;
import static org.asynchttpclient.util.ProxyUtils.getProxyServer;

Expand Down Expand Up @@ -629,6 +630,8 @@ private <T> NettyResponseFuture<T> newNettyResponseFuture(Request request, Async
connectionSemaphore,
proxyServer);

future.setUseAbsoluteRequestDeadline(useAbsoluteRequestDeadline(config, request));

String expectHeader = request.getHeaders().get(EXPECT);
if (HttpHeaderValues.CONTINUE.contentEqualsIgnoreCase(expectHeader)) {
future.setDontWriteBodyBecauseExpectContinue(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,16 @@ public TimeoutsHolder(Timer nettyTimer, NettyResponseFuture<?> nettyResponseFutu
}

if (requestTimeoutInMs > -1) {
requestTimeoutMillisTime = unpreciseMillisTime() + requestTimeoutInMs;
requestTimeout = newTimeout(new RequestTimeoutTimerTask(nettyResponseFuture, requestSender, this, requestTimeoutInMs), requestTimeoutInMs);
// A redirect, a retry or an auth replay builds a new holder for the same future. Anchoring the
// deadline here hands each of those hops a fresh budget, so a chain of n hops runs for n times the
// configured timeout; anchoring it on the future bounds the exchange as a whole instead. Which one
// applies is the caller's choice, per request or per client.
requestTimeoutMillisTime = (nettyResponseFuture.isUseAbsoluteRequestDeadline()
? nettyResponseFuture.getStart() : unpreciseMillisTime()) + requestTimeoutInMs;
// A deadline already behind us is scheduled at zero rather than negative, so the task still runs and
// still cancels its read-timeout sibling, which is bookkeeping only it does.
requestTimeout = newTimeout(new RequestTimeoutTimerTask(nettyResponseFuture, requestSender, this, requestTimeoutInMs),
Math.max(requestTimeoutMillisTime - unpreciseMillisTime(), 0L));
} else {
requestTimeoutMillisTime = -1L;
requestTimeout = null;
Expand Down
5 changes: 5 additions & 0 deletions client/src/main/java/org/asynchttpclient/util/HttpUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ public static boolean followRedirect(AsyncHttpClientConfig config, Request reque
return request.getFollowRedirect() != null ? request.getFollowRedirect() : config.isFollowRedirect();
}

public static boolean useAbsoluteRequestDeadline(AsyncHttpClientConfig config, Request request) {
Boolean override = request.getUseAbsoluteRequestDeadline();
return override != null ? override : config.isUseAbsoluteRequestDeadline();
}

public static ByteBuffer urlEncodeFormParams(List<Param> params, Charset charset) {
return StringUtils.charSequence2ByteBuffer(urlEncodeFormParams0(params, charset), US_ASCII);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ org.asynchttpclient.keepAlive=true
org.asynchttpclient.maxRequestRetry=5
org.asynchttpclient.loadBalance=DEFAULT
org.asynchttpclient.failedIpCooldownEnabled=true
org.asynchttpclient.useAbsoluteRequestDeadline=false
org.asynchttpclient.failedIpCooldownPeriod=PT10S
org.asynchttpclient.disableUrlEncodingForBoundRequests=false
org.asynchttpclient.useLaxCookieEncoder=false
Expand Down
Loading
Loading