HTTPCLIENT-2034: Introduce HttpRequestRetryStrategy

This commit is contained in:
Michael Osipov 2019-12-05 17:57:52 +01:00 committed by Michael Osipov
parent 070f30fdc4
commit 1d56c27e6d
11 changed files with 1220 additions and 28 deletions

View File

@ -30,7 +30,7 @@ import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import org.apache.hc.client5.http.HttpRequestRetryHandler;
import org.apache.hc.client5.http.HttpRequestRetryStrategy;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.protocol.HttpClientContext;
@ -44,6 +44,7 @@ import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpRequestInterceptor;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.impl.io.HttpRequestExecutor;
import org.apache.hc.core5.http.io.HttpClientConnection;
@ -54,6 +55,7 @@ import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.net.URIBuilder;
import org.apache.hc.core5.util.TimeValue;
import org.junit.Assert;
import org.junit.Test;
@ -122,7 +124,7 @@ public class TestClientRequestExecution extends LocalServerTestBase {
};
final HttpRequestRetryHandler requestRetryHandler = new HttpRequestRetryHandler() {
final HttpRequestRetryStrategy requestRetryStrategy = new HttpRequestRetryStrategy() {
@Override
public boolean retryRequest(
@ -133,12 +135,28 @@ public class TestClientRequestExecution extends LocalServerTestBase {
return true;
}
@Override
public boolean retryRequest(
final HttpResponse response,
final int executionCount,
final HttpContext context) {
return false;
}
@Override
public TimeValue getRetryInterval(
final HttpResponse response,
final int executionCount,
final HttpContext context) {
return TimeValue.ofSeconds(1L);
}
};
this.httpclient = this.clientBuilder
.addRequestInterceptorFirst(interceptor)
.setRequestExecutor(new FaultyHttpRequestExecutor("Oppsie"))
.setRetryHandler(requestRetryHandler)
.setRetryStrategy(requestRetryStrategy)
.build();
final HttpHost target = start();
@ -163,7 +181,7 @@ public class TestClientRequestExecution extends LocalServerTestBase {
public void testNonRepeatableEntity() throws Exception {
this.server.registerHandler("*", new SimpleService());
final HttpRequestRetryHandler requestRetryHandler = new HttpRequestRetryHandler() {
final HttpRequestRetryStrategy requestRetryStrategy = new HttpRequestRetryStrategy() {
@Override
public boolean retryRequest(
@ -174,11 +192,27 @@ public class TestClientRequestExecution extends LocalServerTestBase {
return true;
}
@Override
public boolean retryRequest(
final HttpResponse response,
final int executionCount,
final HttpContext context) {
return false;
}
@Override
public TimeValue getRetryInterval(
final HttpResponse response,
final int executionCount,
final HttpContext context) {
return TimeValue.ofSeconds(1L);
}
};
this.httpclient = this.clientBuilder
.setRequestExecutor(new FaultyHttpRequestExecutor("a message showing that this failed"))
.setRetryHandler(requestRetryHandler)
.setRetryStrategy(requestRetryStrategy)
.build();
final HttpHost target = start();

View File

@ -0,0 +1,90 @@
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http;
import java.io.IOException;
import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.util.TimeValue;
/**
* Strategy interface that allows API users to plug in their own logic to
* control whether or not a retry should automatically be done, how many times
* it should be done and so on.
*
* @since 5.0
*/
@Contract(threading = ThreadingBehavior.STATELESS)
public interface HttpRequestRetryStrategy {
/**
* Determines if a method should be retried after an I/O exception
* occured during execution.
*
* @param request the request failed due to an I/O exception
* @param exception the exception that occurred
* @param execCount the number of times this method has been
* unsuccessfully executed
* @param context the context for the request execution
*
* @return {@code true} if the request should be retried, {@code false}
* otherwise
*/
boolean retryRequest(HttpRequest request, IOException exception, int execCount, HttpContext context);
/**
* Determines if a method should be retried given the response from
* the target server.
*
* @param response the response from the target server
* @param execCount the number of times this method has been
* unsuccessfully executed
* @param context the context for the request execution
*
* @return {@code true} if the request should be retried, {@code false}
* otherwise
*/
boolean retryRequest(HttpResponse response, int execCount, HttpContext context);
/**
* Determines the retry interval between subsequent retries.
*
* @param response the response from the target server
* @param execCount the number of times this method has been
* unsuccessfully executed
* @param context the context for the request execution
*
* @return the retry interval between subsequent retries
*/
TimeValue getRetryInterval(HttpResponse response, int execCount, HttpContext context);
}

View File

@ -34,6 +34,7 @@ package org.apache.hc.client5.http.impl;
*/
public enum ChainElements {
REDIRECT, BACK_OFF, RETRY_SERVICE_UNAVAILABLE, RETRY_IO_ERROR, CACHING, PROTOCOL, CONNECT, MAIN_TRANSPORT
REDIRECT, BACK_OFF, RETRY, RETRY_SERVICE_UNAVAILABLE, RETRY_IO_ERROR, CACHING, PROTOCOL,
CONNECT, MAIN_TRANSPORT
}

View File

@ -0,0 +1,232 @@
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.ConnectException;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import javax.net.ssl.SSLException;
import org.apache.hc.client5.http.HttpRequestRetryStrategy;
import org.apache.hc.client5.http.utils.DateUtils;
import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.concurrent.CancellableDependency;
import org.apache.hc.core5.http.ConnectionClosedException;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.Methods;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.util.Args;
import org.apache.hc.core5.util.TimeValue;
/**
* Default implementation of the {@link HttpRequestRetryStrategy} interface.
*
* @since 5.0
*/
@Contract(threading = ThreadingBehavior.STATELESS)
public class DefaultHttpRequestRetryStrategy implements HttpRequestRetryStrategy {
public static final DefaultHttpRequestRetryStrategy INSTANCE = new DefaultHttpRequestRetryStrategy();
/**
* Maximum number of allowed retries
*/
private final int maxRetries;
/**
* Retry interval between subsequent retries
*/
private final TimeValue defaultRetryInterval;
/**
* Derived {@code IOExceptions} which shall not be retried
*/
private final Set<Class<? extends IOException>> nonRetriableIOExceptionClasses;
/**
* HTTP status codes which shall be retried
*/
private final Set<Integer> retriableCodes;
protected DefaultHttpRequestRetryStrategy(
final int maxRetries,
final TimeValue defaultRetryInterval,
final Collection<Class<? extends IOException>> clazzes,
final Collection<Integer> codes) {
Args.notNegative(maxRetries, "maxRetries");
Args.positive(defaultRetryInterval.getDuration(), "defaultRetryInterval");
this.maxRetries = maxRetries;
this.defaultRetryInterval = defaultRetryInterval;
this.nonRetriableIOExceptionClasses = new HashSet<>(clazzes);
this.retriableCodes = new HashSet<>(codes);
}
/**
* Create the HTTP request retry strategy using the following list of
* non-retriable I/O exception classes:<br>
* <ul>
* <li>InterruptedIOException</li>
* <li>UnknownHostException</li>
* <li>ConnectException</li>
* <li>ConnectionClosedException</li>
* <li>SSLException</li>
* </ul>
*
* and retriable HTTP status codes:<br>
* <ul>
* <li>SC_TOO_MANY_REQUESTS (429)</li>
* <li>SC_SERVICE_UNAVAILABLE (503)</li>
* </ul>
*
* @param maxRetries how many times to retry; 0 means no retries
* @param defaultRetryInterval the default retry interval between
* subsequent retries if the {@code Retry-After} header is not set
* or invalid.
*/
public DefaultHttpRequestRetryStrategy(
final int maxRetries,
final TimeValue defaultRetryInterval) {
this(maxRetries, defaultRetryInterval,
Arrays.asList(
InterruptedIOException.class,
UnknownHostException.class,
ConnectException.class,
ConnectionClosedException.class,
SSLException.class),
Arrays.asList(
HttpStatus.SC_TOO_MANY_REQUESTS,
HttpStatus.SC_SERVICE_UNAVAILABLE));
}
/**
* Create the HTTP request retry strategy with a max retry count of 1,
* default retry interval of 1 second, and using the following list of
* non-retriable I/O exception classes:<br>
* <ul>
* <li>InterruptedIOException</li>
* <li>UnknownHostException</li>
* <li>ConnectException</li>
* <li>ConnectionClosedException</li>
* <li>SSLException</li>
* </ul>
*
* and retriable HTTP status codes:<br>
* <ul>
* <li>SC_TOO_MANY_REQUESTS (429)</li>
* <li>SC_SERVICE_UNAVAILABLE (503)</li>
* </ul>
*/
public DefaultHttpRequestRetryStrategy() {
this(1, TimeValue.ofSeconds(1L));
}
@Override
public boolean retryRequest(
final HttpRequest request,
final IOException exception,
final int execCount,
final HttpContext context) {
Args.notNull(request, "request");
Args.notNull(exception, "exception");
if (execCount > this.maxRetries) {
// Do not retry if over max retries
return false;
}
if (this.nonRetriableIOExceptionClasses.contains(exception.getClass())) {
return false;
} else {
for (final Class<? extends IOException> rejectException : this.nonRetriableIOExceptionClasses) {
if (rejectException.isInstance(exception)) {
return false;
}
}
}
if (request instanceof CancellableDependency && ((CancellableDependency) request).isCancelled()) {
return false;
}
// Retry if the request is considered idempotent
return handleAsIdempotent(request);
}
@Override
public boolean retryRequest(
final HttpResponse response,
final int execCount,
final HttpContext context) {
Args.notNull(response, "response");
return execCount <= this.maxRetries && retriableCodes.contains(response.getCode());
}
@Override
public TimeValue getRetryInterval(
final HttpResponse response,
final int execCount,
final HttpContext context) {
Args.notNull(response, "response");
final Header header = response.getFirstHeader(HttpHeaders.RETRY_AFTER);
TimeValue retryAfter = null;
if (header != null) {
final String value = header.getValue();
try {
retryAfter = TimeValue.ofSeconds(Long.parseLong(value));
} catch (final NumberFormatException ignore) {
final Date retryAfterDate = DateUtils.parseDate(value);
if (retryAfterDate != null) {
retryAfter =
TimeValue.ofMilliseconds(retryAfterDate.getTime() - System.currentTimeMillis());
}
}
if (TimeValue.isPositive(retryAfter)) {
return retryAfter;
}
}
return this.defaultRetryInterval;
}
protected boolean handleAsIdempotent(final HttpRequest request) {
return Methods.isIdempotent(request.getMethod());
}
}

View File

@ -0,0 +1,187 @@
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl.async;
import java.io.IOException;
import org.apache.hc.client5.http.HttpRequestRetryStrategy;
import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.async.AsyncExecCallback;
import org.apache.hc.client5.http.async.AsyncExecChain;
import org.apache.hc.client5.http.async.AsyncExecChainHandler;
import org.apache.hc.client5.http.impl.RequestCopier;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.Internal;
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.http.EntityDetails;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.nio.AsyncDataConsumer;
import org.apache.hc.core5.http.nio.AsyncEntityProducer;
import org.apache.hc.core5.http.nio.entity.NoopEntityConsumer;
import org.apache.hc.core5.util.Args;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Request executor in the asynchronous request execution chain that is
* responsible for making a decision whether a request that failed due to
* an I/O exception or received a specific response from the target server should
* be re-executed. Note that this exec chain handler <em>will not</em> respect
* {@link HttpRequestRetryStrategy#getRetryInterval(HttpResponse, int, org.apache.hc.core5.http.protocol.HttpContext)}.
* <p>
* Further responsibilities such as communication with the opposite
* endpoint is delegated to the next executor in the request execution
* chain.
* </p>
*
* @since 5.0
*/
@Contract(threading = ThreadingBehavior.STATELESS)
@Internal
public final class AsyncHttpRequestRetryExec implements AsyncExecChainHandler {
private final Logger log = LoggerFactory.getLogger(getClass());
private final HttpRequestRetryStrategy retryStrategy;
public AsyncHttpRequestRetryExec(final HttpRequestRetryStrategy retryStrategy) {
Args.notNull(retryStrategy, "retryStrategy");
this.retryStrategy = retryStrategy;
}
private static class State {
volatile int execCount;
volatile boolean retrying;
}
private void internalExecute(
final State state,
final HttpRequest request,
final AsyncEntityProducer entityProducer,
final AsyncExecChain.Scope scope,
final AsyncExecChain chain,
final AsyncExecCallback asyncExecCallback) throws HttpException, IOException {
final String exchangeId = scope.exchangeId;
chain.proceed(RequestCopier.INSTANCE.copy(request), entityProducer, scope, new AsyncExecCallback() {
@Override
public AsyncDataConsumer handleResponse(
final HttpResponse response,
final EntityDetails entityDetails) throws HttpException, IOException {
final HttpClientContext clientContext = scope.clientContext;
if (entityProducer != null && !entityProducer.isRepeatable()) {
if (log.isDebugEnabled()) {
log.debug("{}: cannot retry non-repeatable request", exchangeId);
}
return asyncExecCallback.handleResponse(response, entityDetails);
}
state.retrying = retryStrategy.retryRequest(response, state.execCount, clientContext);
if (state.retrying) {
return new NoopEntityConsumer();
} else {
return asyncExecCallback.handleResponse(response, entityDetails);
}
}
@Override
public void handleInformationResponse(final HttpResponse response) throws HttpException, IOException {
asyncExecCallback.handleInformationResponse(response);
}
@Override
public void completed() {
if (state.retrying) {
state.execCount++;
try {
internalExecute(state, request, entityProducer, scope, chain, asyncExecCallback);
} catch (final IOException | HttpException ex) {
asyncExecCallback.failed(ex);
}
} else {
asyncExecCallback.completed();
}
}
@Override
public void failed(final Exception cause) {
if (cause instanceof IOException) {
final HttpRoute route = scope.route;
final HttpClientContext clientContext = scope.clientContext;
if (entityProducer != null && !entityProducer.isRepeatable()) {
if (log.isDebugEnabled()) {
log.debug("{}: cannot retry non-repeatable request", exchangeId);
}
} else if (retryStrategy.retryRequest(request, (IOException) cause, state.execCount, clientContext)) {
if (log.isDebugEnabled()) {
log.debug("{}: {}", exchangeId, cause.getMessage(), cause);
}
if (log.isInfoEnabled()) {
log.info("Recoverable I/O exception ({}) caught when processing request to {}",
cause.getClass().getName(), route);
}
scope.execRuntime.discardEndpoint();
if (entityProducer != null) {
entityProducer.releaseResources();
}
state.retrying = true;
state.execCount++;
try {
internalExecute(state, request, entityProducer, scope, chain, asyncExecCallback);
} catch (final IOException | HttpException ex) {
asyncExecCallback.failed(ex);
}
return;
}
}
asyncExecCallback.failed(cause);
}
});
}
@Override
public void execute(
final HttpRequest request,
final AsyncEntityProducer entityProducer,
final AsyncExecChain.Scope scope,
final AsyncExecChain chain,
final AsyncExecCallback asyncExecCallback) throws HttpException, IOException {
final State state = new State();
state.execCount = 1;
state.retrying = false;
internalExecute(state, request, entityProducer, scope, chain, asyncExecCallback);
}
}

View File

@ -41,6 +41,7 @@ import java.util.concurrent.ThreadFactory;
import org.apache.hc.client5.http.AuthenticationStrategy;
import org.apache.hc.client5.http.DnsResolver;
import org.apache.hc.client5.http.HttpRequestRetryHandler;
import org.apache.hc.client5.http.HttpRequestRetryStrategy;
import org.apache.hc.client5.http.SchemePortResolver;
import org.apache.hc.client5.http.SystemDefaultDnsResolver;
import org.apache.hc.client5.http.async.AsyncExecChainHandler;
@ -55,7 +56,7 @@ import org.apache.hc.client5.http.cookie.CookieStore;
import org.apache.hc.client5.http.impl.ChainElements;
import org.apache.hc.client5.http.impl.CookieSpecSupport;
import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy;
import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryHandler;
import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy;
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
@ -192,6 +193,7 @@ public class H2AsyncClientBuilder {
private HttpRoutePlanner routePlanner;
private RedirectStrategy redirectStrategy;
private HttpRequestRetryHandler retryHandler;
private HttpRequestRetryStrategy retryStrategy;
private Lookup<AuthSchemeProvider> authSchemeRegistry;
private Lookup<CookieSpecProvider> cookieSpecRegistry;
@ -389,6 +391,17 @@ public class H2AsyncClientBuilder {
return this;
}
/**
* Assigns {@link HttpRequestRetryStrategy} instance.
* <p>
* Please note this value can be overridden by the {@link #disableAutomaticRetries()}
* method.
*/
public final H2AsyncClientBuilder setRetryStrategy(final HttpRequestRetryStrategy retryStrategy) {
this.retryStrategy = retryStrategy;
return this;
}
/**
* Assigns {@link RedirectStrategy} instance.
* <p>
@ -674,13 +687,20 @@ public class H2AsyncClientBuilder {
// Add request retry executor, if not disabled
if (!automaticRetriesDisabled) {
HttpRequestRetryHandler retryHandlerCopy = this.retryHandler;
if (retryHandlerCopy == null) {
retryHandlerCopy = DefaultHttpRequestRetryHandler.INSTANCE;
final HttpRequestRetryHandler retryHandlerCopy = this.retryHandler;
if (retryHandlerCopy != null) {
execChainDefinition.addFirst(
new AsyncRetryExec(retryHandlerCopy),
ChainElements.RETRY_IO_ERROR.name());
} else {
HttpRequestRetryStrategy retryStrategyCopy = this.retryStrategy;
if (retryStrategyCopy == null) {
retryStrategyCopy = DefaultHttpRequestRetryStrategy.INSTANCE;
}
execChainDefinition.addFirst(
new AsyncHttpRequestRetryExec(retryStrategyCopy),
ChainElements.RETRY.name());
}
execChainDefinition.addFirst(
new AsyncRetryExec(retryHandlerCopy),
ChainElements.RETRY_IO_ERROR.name());
}
HttpRoutePlanner routePlannerCopy = this.routePlanner;

View File

@ -41,6 +41,7 @@ import java.util.concurrent.ThreadFactory;
import org.apache.hc.client5.http.AuthenticationStrategy;
import org.apache.hc.client5.http.ConnectionKeepAliveStrategy;
import org.apache.hc.client5.http.HttpRequestRetryHandler;
import org.apache.hc.client5.http.HttpRequestRetryStrategy;
import org.apache.hc.client5.http.SchemePortResolver;
import org.apache.hc.client5.http.SystemDefaultDnsResolver;
import org.apache.hc.client5.http.UserTokenHandler;
@ -57,7 +58,7 @@ import org.apache.hc.client5.http.impl.ChainElements;
import org.apache.hc.client5.http.impl.CookieSpecSupport;
import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy;
import org.apache.hc.client5.http.impl.DefaultConnectionKeepAliveStrategy;
import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryHandler;
import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy;
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
import org.apache.hc.client5.http.impl.DefaultUserTokenHandler;
@ -227,6 +228,7 @@ public class HttpAsyncClientBuilder {
private HttpRoutePlanner routePlanner;
private RedirectStrategy redirectStrategy;
private HttpRequestRetryHandler retryHandler;
private HttpRequestRetryStrategy retryStrategy;
private ConnectionReuseStrategy reuseStrategy;
@ -496,6 +498,17 @@ public class HttpAsyncClientBuilder {
return this;
}
/**
* Assigns {@link HttpRequestRetryStrategy} instance.
* <p>
* Please note this value can be overridden by the {@link #disableAutomaticRetries()}
* method.
*/
public final HttpAsyncClientBuilder setRetryStrategy(final HttpRequestRetryStrategy retryStrategy) {
this.retryStrategy = retryStrategy;
return this;
}
/**
* Assigns {@link RedirectStrategy} instance.
* <p>
@ -824,13 +837,20 @@ public class HttpAsyncClientBuilder {
// Add request retry executor, if not disabled
if (!automaticRetriesDisabled) {
HttpRequestRetryHandler retryHandlerCopy = this.retryHandler;
if (retryHandlerCopy == null) {
retryHandlerCopy = DefaultHttpRequestRetryHandler.INSTANCE;
final HttpRequestRetryHandler retryHandlerCopy = this.retryHandler;
if (retryHandlerCopy != null) {
execChainDefinition.addFirst(
new AsyncRetryExec(retryHandlerCopy),
ChainElements.RETRY_IO_ERROR.name());
} else {
HttpRequestRetryStrategy retryStrategyCopy = this.retryStrategy;
if (retryStrategyCopy == null) {
retryStrategyCopy = DefaultHttpRequestRetryStrategy.INSTANCE;
}
execChainDefinition.addFirst(
new AsyncHttpRequestRetryExec(retryStrategyCopy),
ChainElements.RETRY.name());
}
execChainDefinition.addFirst(
new AsyncRetryExec(retryHandlerCopy),
ChainElements.RETRY_IO_ERROR.name());
}
HttpRoutePlanner routePlannerCopy = this.routePlanner;

View File

@ -40,6 +40,7 @@ import java.util.Map;
import org.apache.hc.client5.http.AuthenticationStrategy;
import org.apache.hc.client5.http.ConnectionKeepAliveStrategy;
import org.apache.hc.client5.http.HttpRequestRetryHandler;
import org.apache.hc.client5.http.HttpRequestRetryStrategy;
import org.apache.hc.client5.http.SchemePortResolver;
import org.apache.hc.client5.http.ServiceUnavailableRetryStrategy;
import org.apache.hc.client5.http.SystemDefaultDnsResolver;
@ -60,7 +61,7 @@ import org.apache.hc.client5.http.impl.ChainElements;
import org.apache.hc.client5.http.impl.CookieSpecSupport;
import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy;
import org.apache.hc.client5.http.impl.DefaultConnectionKeepAliveStrategy;
import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryHandler;
import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy;
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
import org.apache.hc.client5.http.impl.DefaultUserTokenHandler;
@ -204,6 +205,7 @@ public class HttpClientBuilder {
private LinkedList<ExecInterceptorEntry> execInterceptors;
private HttpRequestRetryHandler retryHandler;
private HttpRequestRetryStrategy retryStrategy;
private HttpRoutePlanner routePlanner;
private RedirectStrategy redirectStrategy;
private ConnectionBackoffStrategy connectionBackoffStrategy;
@ -505,6 +507,17 @@ public class HttpClientBuilder {
return this;
}
/**
* Assigns {@link HttpRequestRetryStrategy} instance.
* <p>
* Please note this value can be overridden by the {@link #disableAutomaticRetries()}
* method.
*/
public final HttpClientBuilder setRetryStrategy(final HttpRequestRetryStrategy retryStrategy) {
this.retryStrategy = retryStrategy;
return this;
}
/**
* Disables automatic request recovery and re-execution.
*/
@ -859,13 +872,21 @@ public class HttpClientBuilder {
// Add request retry executor, if not disabled
if (!automaticRetriesDisabled) {
HttpRequestRetryHandler retryHandlerCopy = this.retryHandler;
if (retryHandlerCopy == null) {
retryHandlerCopy = DefaultHttpRequestRetryHandler.INSTANCE;
// This needs to be cleaned up as soon as HttpRequestRetryHandler will be removed
final HttpRequestRetryHandler retryHandlerCopy = this.retryHandler;
if (retryHandlerCopy != null) {
execChainDefinition.addFirst(
new RetryExec(retryHandlerCopy),
ChainElements.RETRY_IO_ERROR.name());
} else {
HttpRequestRetryStrategy retryStrategyCopy = this.retryStrategy;
if (retryStrategyCopy == null) {
retryStrategyCopy = DefaultHttpRequestRetryStrategy.INSTANCE;
}
execChainDefinition.addFirst(
new HttpRequestRetryExec(retryStrategyCopy),
ChainElements.RETRY.name());
}
execChainDefinition.addFirst(
new RetryExec(retryHandlerCopy),
ChainElements.RETRY_IO_ERROR.name());
}
HttpRoutePlanner routePlannerCopy = this.routePlanner;

View File

@ -0,0 +1,160 @@
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl.classic;
import java.io.IOException;
import java.io.InterruptedIOException;
import org.apache.hc.client5.http.HttpRequestRetryStrategy;
import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.classic.ExecChain;
import org.apache.hc.client5.http.classic.ExecChain.Scope;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.classic.ExecChainHandler;
import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.Internal;
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.NoHttpResponseException;
import org.apache.hc.core5.util.Args;
import org.apache.hc.core5.util.TimeValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Request executor in the request execution chain that is responsible for
* making a decision whether a request that failed due to an I/O exception
* or received a specific response from the target server should
* be re-executed.
* <p>
* Further responsibilities such as communication with the opposite
* endpoint is delegated to the next executor in the request execution
* chain.
* </p>
*
* @since 5.0
*/
@Contract(threading = ThreadingBehavior.STATELESS)
@Internal
public class HttpRequestRetryExec implements ExecChainHandler {
private final Logger log = LoggerFactory.getLogger(getClass());
private final HttpRequestRetryStrategy retryStrategy;
public HttpRequestRetryExec(
final HttpRequestRetryStrategy retryStrategy) {
Args.notNull(retryStrategy, "retryStrategy");
this.retryStrategy = retryStrategy;
}
@Override
public ClassicHttpResponse execute(
final ClassicHttpRequest request,
final Scope scope,
final ExecChain chain) throws IOException, HttpException {
Args.notNull(request, "request");
Args.notNull(scope, "scope");
final String exchangeId = scope.exchangeId;
final HttpRoute route = scope.route;
final HttpClientContext context = scope.clientContext;
ClassicHttpRequest currentRequest = request;
for (int execCount = 1;; execCount++) {
final ClassicHttpResponse response;
try {
response = chain.proceed(currentRequest, scope);
} catch (final IOException ex) {
if (scope.execRuntime.isExecutionAborted()) {
throw new RequestFailedException("Request aborted");
}
final HttpEntity requestEntity = request.getEntity();
if (requestEntity != null && !requestEntity.isRepeatable()) {
if (log.isDebugEnabled()) {
log.debug("{}: cannot retry non-repeatable request", exchangeId);
}
throw ex;
}
if (retryStrategy.retryRequest(request, ex, execCount, context)) {
if (log.isDebugEnabled()) {
log.debug("{}: {}", exchangeId, ex.getMessage(), ex);
}
if (log.isInfoEnabled()) {
log.info("Recoverable I/O exception ({}) caught when processing request to {}",
ex.getClass().getName(), route);
}
currentRequest = ClassicRequestCopier.INSTANCE.copy(scope.originalRequest);
continue;
} else {
if (ex instanceof NoHttpResponseException) {
final NoHttpResponseException updatedex = new NoHttpResponseException(
route.getTargetHost().toHostString() + " failed to respond");
updatedex.setStackTrace(ex.getStackTrace());
throw updatedex;
}
throw ex;
}
}
try {
final HttpEntity entity = request.getEntity();
if (entity != null && !entity.isRepeatable()) {
if (log.isDebugEnabled()) {
log.debug("{}: cannot retry non-repeatable request", exchangeId);
}
return response;
}
if (retryStrategy.retryRequest(response, execCount, context)) {
response.close();
final TimeValue nextInterval =
retryStrategy.getRetryInterval(response, execCount, context);
if (TimeValue.isPositive(nextInterval)) {
try {
if (log.isDebugEnabled()) {
log.debug("{}: wait for {}", exchangeId, nextInterval);
}
nextInterval.sleep();
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
throw new InterruptedIOException();
}
}
currentRequest = ClassicRequestCopier.INSTANCE.copy(scope.originalRequest);
} else {
return response;
}
} catch (final RuntimeException ex) {
response.close();
throw ex;
}
}
}
}

View File

@ -0,0 +1,158 @@
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl;
import java.io.IOException;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.Date;
import javax.net.ssl.SSLException;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.utils.DateUtils;
import org.apache.hc.core5.http.ConnectionClosedException;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.message.BasicHttpResponse;
import org.apache.hc.core5.util.TimeValue;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
public class TestDefaultHttpRequestRetryStrategy {
private DefaultHttpRequestRetryStrategy retryStrategy;
@Before
public void setup() {
this.retryStrategy = new DefaultHttpRequestRetryStrategy(3, TimeValue.ofMilliseconds(1234L));
}
@Test
public void testBasics() throws Exception {
final HttpResponse response1 = new BasicHttpResponse(503, "Oppsie");
Assert.assertTrue(this.retryStrategy.retryRequest(response1, 1, null));
Assert.assertTrue(this.retryStrategy.retryRequest(response1, 2, null));
Assert.assertTrue(this.retryStrategy.retryRequest(response1, 3, null));
Assert.assertFalse(this.retryStrategy.retryRequest(response1, 4, null));
final HttpResponse response2 = new BasicHttpResponse(500, "Big Time Oppsie");
Assert.assertFalse(this.retryStrategy.retryRequest(response2, 1, null));
final HttpResponse response3 = new BasicHttpResponse(429, "Oppsie");
Assert.assertTrue(this.retryStrategy.retryRequest(response3, 1, null));
Assert.assertTrue(this.retryStrategy.retryRequest(response3, 2, null));
Assert.assertTrue(this.retryStrategy.retryRequest(response3, 3, null));
Assert.assertFalse(this.retryStrategy.retryRequest(response3, 4, null));
Assert.assertEquals(TimeValue.ofMilliseconds(1234L), this.retryStrategy.getRetryInterval(response1, 1, null));
}
@Test
public void testRetryAfterHeaderAsLong() throws Exception {
final HttpResponse response = new BasicHttpResponse(503, "Oppsie");
response.setHeader(HttpHeaders.RETRY_AFTER, "321");
Assert.assertEquals(TimeValue.ofSeconds(321L), this.retryStrategy.getRetryInterval(response, 3, null));
}
@Test
public void testRetryAfterHeaderAsDate() throws Exception {
this.retryStrategy = new DefaultHttpRequestRetryStrategy(3, TimeValue.ofMilliseconds(1L));
final HttpResponse response = new BasicHttpResponse(503, "Oppsie");
response.setHeader(HttpHeaders.RETRY_AFTER, DateUtils.formatDate(new Date(System.currentTimeMillis() + 100000L)));
Assert.assertTrue(this.retryStrategy.getRetryInterval(response, 3, null).compareTo(TimeValue.ofMilliseconds(1L)) > 0);
}
@Test
public void testRetryAfterHeaderAsPastDate() throws Exception {
final HttpResponse response = new BasicHttpResponse(503, "Oppsie");
response.setHeader(HttpHeaders.RETRY_AFTER, DateUtils.formatDate(new Date(System.currentTimeMillis() - 100000L)));
Assert.assertEquals(TimeValue.ofMilliseconds(1234L), this.retryStrategy.getRetryInterval(response, 3, null));
}
@Test
public void testInvalidRetryAfterHeader() throws Exception {
final HttpResponse response = new BasicHttpResponse(503, "Oppsie");
response.setHeader(HttpHeaders.RETRY_AFTER, "Stuff");
Assert.assertEquals(TimeValue.ofMilliseconds(1234L), retryStrategy.getRetryInterval(response, 3, null));
}
@Test
public void noRetryOnConnectTimeout() throws Exception {
final HttpGet request = new HttpGet("/");
Assert.assertFalse(retryStrategy.retryRequest(request, new SocketTimeoutException(), 1, null));
}
@Test
public void noRetryOnConnect() throws Exception {
final HttpGet request = new HttpGet("/");
Assert.assertFalse(retryStrategy.retryRequest(request, new ConnectException(), 1, null));
}
@Test
public void noRetryOnConnectionClosed() throws Exception {
final HttpGet request = new HttpGet("/");
Assert.assertFalse(retryStrategy.retryRequest(request, new ConnectionClosedException(), 1, null));
}
@Test
public void noRetryOnSSLFailure() throws Exception {
final HttpGet request = new HttpGet("/");
Assert.assertFalse(retryStrategy.retryRequest(request, new SSLException("encryption failed"), 1, null));
}
@Test
public void noRetryOnUnknownHost() throws Exception {
final HttpGet request = new HttpGet("/");
Assert.assertFalse(retryStrategy.retryRequest(request, new UnknownHostException(), 1, null));
}
@Test
public void noRetryOnAbortedRequests() throws Exception {
final HttpGet request = new HttpGet("/");
request.cancel();
Assert.assertFalse(retryStrategy.retryRequest(request, new IOException(), 1, null));
}
@Test
public void retryOnNonAbortedRequests() throws Exception {
final HttpGet request = new HttpGet("/");
Assert.assertTrue(retryStrategy.retryRequest(request, new IOException(), 1, null));
}
}

View File

@ -0,0 +1,269 @@
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl.classic;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.apache.hc.client5.http.HttpRequestRetryStrategy;
import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.classic.ExecChain;
import org.apache.hc.client5.http.classic.ExecRuntime;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.entity.EntityBuilder;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.util.TimeValue;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
@SuppressWarnings({"boxing","static-access"}) // test code
public class TestHttpRequestRetryExec {
@Mock
private HttpRequestRetryStrategy retryStrategy;
@Mock
private ExecChain chain;
@Mock
private ExecRuntime endpoint;
private HttpRequestRetryExec retryExec;
private HttpHost target;
@Before
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
retryExec = new HttpRequestRetryExec(retryStrategy);
target = new HttpHost("localhost", 80);
}
@Test
public void testFundamentals1() throws Exception {
final HttpRoute route = new HttpRoute(target);
final HttpGet request = new HttpGet("/test");
final HttpClientContext context = HttpClientContext.create();
final ClassicHttpResponse response = Mockito.mock(ClassicHttpResponse.class);
Mockito.when(chain.proceed(
Mockito.same(request),
Mockito.<ExecChain.Scope>any())).thenReturn(response);
Mockito.when(retryStrategy.retryRequest(
Mockito.<HttpResponse>any(),
Mockito.anyInt(),
Mockito.<HttpContext>any())).thenReturn(Boolean.TRUE, Boolean.FALSE);
Mockito.when(retryStrategy.getRetryInterval(
Mockito.<HttpResponse>any(),
Mockito.anyInt(),
Mockito.<HttpContext>any())).thenReturn(TimeValue.ZERO_MILLISECONDS);
final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
retryExec.execute(request, scope, chain);
Mockito.verify(chain, Mockito.times(2)).proceed(
Mockito.<ClassicHttpRequest>any(),
Mockito.same(scope));
Mockito.verify(response, Mockito.times(1)).close();
}
@Test(expected = RuntimeException.class)
public void testStrategyRuntimeException() throws Exception {
final HttpRoute route = new HttpRoute(target);
final ClassicHttpRequest request = new HttpGet("/test");
final HttpClientContext context = HttpClientContext.create();
final ClassicHttpResponse response = Mockito.mock(ClassicHttpResponse.class);
Mockito.when(chain.proceed(
Mockito.<ClassicHttpRequest>any(),
Mockito.<ExecChain.Scope>any())).thenReturn(response);
Mockito.doThrow(new RuntimeException("Ooopsie")).when(retryStrategy).retryRequest(
Mockito.<HttpResponse>any(),
Mockito.anyInt(),
Mockito.<HttpContext>any());
final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
try {
retryExec.execute(request, scope, chain);
} catch (final Exception ex) {
Mockito.verify(response).close();
throw ex;
}
}
@Test
public void testNonRepeatableEntityResponseReturnedImmediately() throws Exception {
final HttpRoute route = new HttpRoute(target);
final HttpPost request = new HttpPost("/test");
request.setEntity(EntityBuilder.create()
.setStream(new ByteArrayInputStream(new byte[]{}))
.build());
final HttpClientContext context = HttpClientContext.create();
final ClassicHttpResponse response = Mockito.mock(ClassicHttpResponse.class);
Mockito.when(chain.proceed(
Mockito.<ClassicHttpRequest>any(),
Mockito.<ExecChain.Scope>any())).thenReturn(response);
Mockito.when(retryStrategy.retryRequest(
Mockito.<HttpResponse>any(),
Mockito.anyInt(),
Mockito.<HttpContext>any())).thenReturn(Boolean.TRUE, Boolean.FALSE);
final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
final ClassicHttpResponse finalResponse = retryExec.execute(request, scope, chain);
Assert.assertSame(response, finalResponse);
Mockito.verify(response, Mockito.times(0)).close();
}
@Test(expected = IOException.class)
public void testFundamentals2() throws Exception {
final HttpRoute route = new HttpRoute(target);
final HttpGet originalRequest = new HttpGet("/test");
originalRequest.addHeader("header", "this");
originalRequest.addHeader("header", "that");
final HttpClientContext context = HttpClientContext.create();
Mockito.when(chain.proceed(
Mockito.<ClassicHttpRequest>any(),
Mockito.<ExecChain.Scope>any())).thenAnswer(new Answer<Object>() {
@Override
public Object answer(final InvocationOnMock invocationOnMock) throws Throwable {
final Object[] args = invocationOnMock.getArguments();
final ClassicHttpRequest wrapper = (ClassicHttpRequest) args[0];
final Header[] headers = wrapper.getHeaders();
Assert.assertEquals(2, headers.length);
Assert.assertEquals("this", headers[0].getValue());
Assert.assertEquals("that", headers[1].getValue());
wrapper.addHeader("Cookie", "monster");
throw new IOException("Ka-boom");
}
});
Mockito.when(retryStrategy.retryRequest(
Mockito.<HttpRequest>any(),
Mockito.<IOException>any(),
Mockito.eq(1),
Mockito.<HttpContext>any())).thenReturn(Boolean.TRUE);
final ExecChain.Scope scope = new ExecChain.Scope("test", route, originalRequest, endpoint, context);
final ClassicHttpRequest request = ClassicRequestCopier.INSTANCE.copy(originalRequest);
try {
retryExec.execute(request, scope, chain);
} catch (final IOException ex) {
Mockito.verify(chain, Mockito.times(2)).proceed(
Mockito.<ClassicHttpRequest>any(),
Mockito.same(scope));
throw ex;
}
}
@Test(expected = IOException.class)
public void testAbortedRequest() throws Exception {
final HttpRoute route = new HttpRoute(target);
final HttpGet originalRequest = new HttpGet("/test");
final HttpClientContext context = HttpClientContext.create();
Mockito.when(chain.proceed(
Mockito.<ClassicHttpRequest>any(),
Mockito.<ExecChain.Scope>any())).thenThrow(new IOException("Ka-boom"));
Mockito.when(endpoint.isExecutionAborted()).thenReturn(true);
final ExecChain.Scope scope = new ExecChain.Scope("test", route, originalRequest, endpoint, context);
final ClassicHttpRequest request = ClassicRequestCopier.INSTANCE.copy(originalRequest);
try {
retryExec.execute(request, scope, chain);
} catch (final IOException ex) {
Mockito.verify(chain, Mockito.times(1)).proceed(
Mockito.same(request),
Mockito.same(scope));
Mockito.verify(retryStrategy, Mockito.never()).retryRequest(
Mockito.<HttpRequest>any(),
Mockito.<IOException>any(),
Mockito.anyInt(),
Mockito.<HttpContext>any());
throw ex;
}
}
@Test(expected = IOException.class)
public void testNonRepeatableRequest() throws Exception {
final HttpRoute route = new HttpRoute(target);
final HttpPost originalRequest = new HttpPost("/test");
originalRequest.setEntity(EntityBuilder.create()
.setStream(new ByteArrayInputStream(new byte[]{}))
.build());
final HttpClientContext context = HttpClientContext.create();
Mockito.when(chain.proceed(
Mockito.<ClassicHttpRequest>any(),
Mockito.<ExecChain.Scope>any())).thenAnswer(new Answer<Object>() {
@Override
public Object answer(final InvocationOnMock invocationOnMock) throws Throwable {
final Object[] args = invocationOnMock.getArguments();
final ClassicHttpRequest req = (ClassicHttpRequest) args[0];
req.getEntity().writeTo(new ByteArrayOutputStream());
throw new IOException("Ka-boom");
}
});
Mockito.when(retryStrategy.retryRequest(
Mockito.<HttpRequest>any(),
Mockito.<IOException>any(),
Mockito.eq(1),
Mockito.<HttpContext>any())).thenReturn(Boolean.TRUE);
final ExecChain.Scope scope = new ExecChain.Scope("test", route, originalRequest, endpoint, context);
final ClassicHttpRequest request = ClassicRequestCopier.INSTANCE.copy(originalRequest);
try {
retryExec.execute(request, scope, chain);
} catch (final IOException ex) {
Mockito.verify(chain, Mockito.times(1)).proceed(
Mockito.same(request),
Mockito.same(scope));
throw ex;
}
}
}