-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Bound an exchange by one request timeout #2314
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the part we cannot take back later. A 27 arg public constructor gets pinned by revapi, the next per request option makes it 28, and the two parameter lists have to stay in lockstep with no compiler help if they drift, since the tail is all reference types. Only RequestBuilderBase.build calls it. Package private, or a small options holder, costs nothing today. |
||
| 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; | ||
|
|
@@ -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; | ||
|
|
@@ -232,6 +273,11 @@ public List<Part> getBodyParts() { | |
| return followRedirect; | ||
| } | ||
|
|
||
| @Override | ||
| public @Nullable Boolean getUseAbsoluteRequestDeadline() { | ||
| return useAbsoluteRequestDeadline; | ||
| } | ||
|
|
||
| @Override | ||
| public Duration getRequestTimeout() { | ||
| return requestTimeout; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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(); | ||
|
|
@@ -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(); | ||
|
|
@@ -685,6 +698,7 @@ private RequestBuilderBase<?> executeSignatureCalculator() { | |
| rb.realm = realm; | ||
| rb.file = file; | ||
| rb.followRedirect = followRedirect; | ||
| rb.useAbsoluteRequestDeadline = useAbsoluteRequestDeadline; | ||
| rb.requestTimeout = requestTimeout; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since you are already in this copy block, readTimeout is missing from it. Set a signature calculator and a per request read timeout silently falls back to the config 60s. Exactly the hand copied field list problem this PR is about. |
||
| rb.rangeOffset = rangeOffset; | ||
| rb.charset = charset; | ||
|
|
@@ -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 |
|---|---|---|
|
|
@@ -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"; | ||
|
|
@@ -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() { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nothing asserts this default. AsyncHttpClientDefaultsTest covers the other flags, and if the key is ever dropped or misspelled getBoolean returns false quietly, which is also the intended default, so the regression stays invisible until someone flips it. A direct TimeoutsHolder test for the anchor and the clamp would help too, the five new tests are all wall clock. |
||
| 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); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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, | ||
|
|
@@ -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() { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Holding it on the future is the right call, but the Request now disagrees with the behaviour. After a redirect, getTargetRequest().getUseAbsoluteRequestDeadline() is null, so a filter, a signature calculator or a handler reading the request thinks we are on per attempt timeouts while the future is enforcing a deadline. Redirect30xInterceptor can carry it across for free. While in there, it also drops readTimeout, which resets a 500ms read timeout to the config default on every hop after the first. |
||
| return useAbsoluteRequestDeadline; | ||
| } | ||
|
|
||
| public void setUseAbsoluteRequestDeadline(boolean useAbsoluteRequestDeadline) { | ||
| this.useAbsoluteRequestDeadline = useAbsoluteRequestDeadline; | ||
| } | ||
|
|
||
| public Realm getRealm() { | ||
| return realm; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. getStart is currentTimeMillis, but the timer that actually fires runs on nanoTime. Until now a clock step could only distort the hop in flight. With an absolute anchor, a backward NTP correction of a few seconds effectively removes a 600ms deadline for the rest of the chain, and a forward one aborts every later hop instantly on a healthy connection. An absolute anchor really wants a monotonic source. |
||
| ? 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. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This justification does not hold. readTimeout is still null at this point, it is only set once scheduleReadTimeout runs after the write, and requestTimeout is not assigned yet either because the constructor registers with the timer first. In the case you are describing the task cancels nothing. |
||
| requestTimeout = newTimeout(new RequestTimeoutTimerTask(nettyResponseFuture, requestSender, this, requestTimeoutInMs), | ||
| Math.max(requestTimeoutMillisTime - unpreciseMillisTime(), 0L)); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clamping to zero does not stop the hop, it only delays the abort. The wheel timer fires on tick boundaries, so with a deadline already behind us we still take a permit, still take a connection and still write the request, then fail it a tick later. On a 307 that means the POST body reached the redirect target while the caller is handed a TimeoutException that reads like nothing was sent. I think this wants a remaining budget check in sendNextRequest that fails the exchange before the write. |
||
| } else { | ||
| requestTimeoutMillisTime = -1L; | ||
| requestTimeout = null; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hardcoding false here means the property only ever works through the builder. Someone on a custom config or a wrapper sets org.asynchttpclient.useAbsoluteRequestDeadline, gets nothing, and nothing is logged, while a sibling service on the builder honours it. Either delegate to defaultUseAbsoluteRequestDeadline or say in the javadoc that the property is builder only.