HTTPCLIENT-2034: Introduce HttpRequestRetryStrategy
This commit is contained in:
parent
070f30fdc4
commit
1d56c27e6d
|
@ -30,7 +30,7 @@ import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
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.HttpGet;
|
||||||
import org.apache.hc.client5.http.classic.methods.HttpPost;
|
import org.apache.hc.client5.http.classic.methods.HttpPost;
|
||||||
import org.apache.hc.client5.http.protocol.HttpClientContext;
|
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.HttpHost;
|
||||||
import org.apache.hc.core5.http.HttpRequest;
|
import org.apache.hc.core5.http.HttpRequest;
|
||||||
import org.apache.hc.core5.http.HttpRequestInterceptor;
|
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.HttpStatus;
|
||||||
import org.apache.hc.core5.http.impl.io.HttpRequestExecutor;
|
import org.apache.hc.core5.http.impl.io.HttpRequestExecutor;
|
||||||
import org.apache.hc.core5.http.io.HttpClientConnection;
|
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.message.BasicClassicHttpRequest;
|
||||||
import org.apache.hc.core5.http.protocol.HttpContext;
|
import org.apache.hc.core5.http.protocol.HttpContext;
|
||||||
import org.apache.hc.core5.net.URIBuilder;
|
import org.apache.hc.core5.net.URIBuilder;
|
||||||
|
import org.apache.hc.core5.util.TimeValue;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
@ -122,7 +124,7 @@ public class TestClientRequestExecution extends LocalServerTestBase {
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
final HttpRequestRetryHandler requestRetryHandler = new HttpRequestRetryHandler() {
|
final HttpRequestRetryStrategy requestRetryStrategy = new HttpRequestRetryStrategy() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean retryRequest(
|
public boolean retryRequest(
|
||||||
|
@ -133,12 +135,28 @@ public class TestClientRequestExecution extends LocalServerTestBase {
|
||||||
return true;
|
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
|
this.httpclient = this.clientBuilder
|
||||||
.addRequestInterceptorFirst(interceptor)
|
.addRequestInterceptorFirst(interceptor)
|
||||||
.setRequestExecutor(new FaultyHttpRequestExecutor("Oppsie"))
|
.setRequestExecutor(new FaultyHttpRequestExecutor("Oppsie"))
|
||||||
.setRetryHandler(requestRetryHandler)
|
.setRetryStrategy(requestRetryStrategy)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
final HttpHost target = start();
|
final HttpHost target = start();
|
||||||
|
@ -163,7 +181,7 @@ public class TestClientRequestExecution extends LocalServerTestBase {
|
||||||
public void testNonRepeatableEntity() throws Exception {
|
public void testNonRepeatableEntity() throws Exception {
|
||||||
this.server.registerHandler("*", new SimpleService());
|
this.server.registerHandler("*", new SimpleService());
|
||||||
|
|
||||||
final HttpRequestRetryHandler requestRetryHandler = new HttpRequestRetryHandler() {
|
final HttpRequestRetryStrategy requestRetryStrategy = new HttpRequestRetryStrategy() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean retryRequest(
|
public boolean retryRequest(
|
||||||
|
@ -174,11 +192,27 @@ public class TestClientRequestExecution extends LocalServerTestBase {
|
||||||
return true;
|
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
|
this.httpclient = this.clientBuilder
|
||||||
.setRequestExecutor(new FaultyHttpRequestExecutor("a message showing that this failed"))
|
.setRequestExecutor(new FaultyHttpRequestExecutor("a message showing that this failed"))
|
||||||
.setRetryHandler(requestRetryHandler)
|
.setRetryStrategy(requestRetryStrategy)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
final HttpHost target = start();
|
final HttpHost target = start();
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
}
|
|
@ -34,6 +34,7 @@ package org.apache.hc.client5.http.impl;
|
||||||
*/
|
*/
|
||||||
public enum ChainElements {
|
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
|
||||||
|
|
||||||
}
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -41,6 +41,7 @@ import java.util.concurrent.ThreadFactory;
|
||||||
import org.apache.hc.client5.http.AuthenticationStrategy;
|
import org.apache.hc.client5.http.AuthenticationStrategy;
|
||||||
import org.apache.hc.client5.http.DnsResolver;
|
import org.apache.hc.client5.http.DnsResolver;
|
||||||
import org.apache.hc.client5.http.HttpRequestRetryHandler;
|
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.SchemePortResolver;
|
||||||
import org.apache.hc.client5.http.SystemDefaultDnsResolver;
|
import org.apache.hc.client5.http.SystemDefaultDnsResolver;
|
||||||
import org.apache.hc.client5.http.async.AsyncExecChainHandler;
|
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.ChainElements;
|
||||||
import org.apache.hc.client5.http.impl.CookieSpecSupport;
|
import org.apache.hc.client5.http.impl.CookieSpecSupport;
|
||||||
import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy;
|
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.DefaultRedirectStrategy;
|
||||||
import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
|
import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
|
||||||
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
|
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
|
||||||
|
@ -192,6 +193,7 @@ public class H2AsyncClientBuilder {
|
||||||
private HttpRoutePlanner routePlanner;
|
private HttpRoutePlanner routePlanner;
|
||||||
private RedirectStrategy redirectStrategy;
|
private RedirectStrategy redirectStrategy;
|
||||||
private HttpRequestRetryHandler retryHandler;
|
private HttpRequestRetryHandler retryHandler;
|
||||||
|
private HttpRequestRetryStrategy retryStrategy;
|
||||||
|
|
||||||
private Lookup<AuthSchemeProvider> authSchemeRegistry;
|
private Lookup<AuthSchemeProvider> authSchemeRegistry;
|
||||||
private Lookup<CookieSpecProvider> cookieSpecRegistry;
|
private Lookup<CookieSpecProvider> cookieSpecRegistry;
|
||||||
|
@ -389,6 +391,17 @@ public class H2AsyncClientBuilder {
|
||||||
return this;
|
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.
|
* Assigns {@link RedirectStrategy} instance.
|
||||||
* <p>
|
* <p>
|
||||||
|
@ -674,13 +687,20 @@ public class H2AsyncClientBuilder {
|
||||||
|
|
||||||
// Add request retry executor, if not disabled
|
// Add request retry executor, if not disabled
|
||||||
if (!automaticRetriesDisabled) {
|
if (!automaticRetriesDisabled) {
|
||||||
HttpRequestRetryHandler retryHandlerCopy = this.retryHandler;
|
final HttpRequestRetryHandler retryHandlerCopy = this.retryHandler;
|
||||||
if (retryHandlerCopy == null) {
|
if (retryHandlerCopy != null) {
|
||||||
retryHandlerCopy = DefaultHttpRequestRetryHandler.INSTANCE;
|
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;
|
HttpRoutePlanner routePlannerCopy = this.routePlanner;
|
||||||
|
|
|
@ -41,6 +41,7 @@ import java.util.concurrent.ThreadFactory;
|
||||||
import org.apache.hc.client5.http.AuthenticationStrategy;
|
import org.apache.hc.client5.http.AuthenticationStrategy;
|
||||||
import org.apache.hc.client5.http.ConnectionKeepAliveStrategy;
|
import org.apache.hc.client5.http.ConnectionKeepAliveStrategy;
|
||||||
import org.apache.hc.client5.http.HttpRequestRetryHandler;
|
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.SchemePortResolver;
|
||||||
import org.apache.hc.client5.http.SystemDefaultDnsResolver;
|
import org.apache.hc.client5.http.SystemDefaultDnsResolver;
|
||||||
import org.apache.hc.client5.http.UserTokenHandler;
|
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.CookieSpecSupport;
|
||||||
import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy;
|
import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy;
|
||||||
import org.apache.hc.client5.http.impl.DefaultConnectionKeepAliveStrategy;
|
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.DefaultRedirectStrategy;
|
||||||
import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
|
import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
|
||||||
import org.apache.hc.client5.http.impl.DefaultUserTokenHandler;
|
import org.apache.hc.client5.http.impl.DefaultUserTokenHandler;
|
||||||
|
@ -227,6 +228,7 @@ public class HttpAsyncClientBuilder {
|
||||||
private HttpRoutePlanner routePlanner;
|
private HttpRoutePlanner routePlanner;
|
||||||
private RedirectStrategy redirectStrategy;
|
private RedirectStrategy redirectStrategy;
|
||||||
private HttpRequestRetryHandler retryHandler;
|
private HttpRequestRetryHandler retryHandler;
|
||||||
|
private HttpRequestRetryStrategy retryStrategy;
|
||||||
|
|
||||||
private ConnectionReuseStrategy reuseStrategy;
|
private ConnectionReuseStrategy reuseStrategy;
|
||||||
|
|
||||||
|
@ -496,6 +498,17 @@ public class HttpAsyncClientBuilder {
|
||||||
return this;
|
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.
|
* Assigns {@link RedirectStrategy} instance.
|
||||||
* <p>
|
* <p>
|
||||||
|
@ -824,13 +837,20 @@ public class HttpAsyncClientBuilder {
|
||||||
|
|
||||||
// Add request retry executor, if not disabled
|
// Add request retry executor, if not disabled
|
||||||
if (!automaticRetriesDisabled) {
|
if (!automaticRetriesDisabled) {
|
||||||
HttpRequestRetryHandler retryHandlerCopy = this.retryHandler;
|
final HttpRequestRetryHandler retryHandlerCopy = this.retryHandler;
|
||||||
if (retryHandlerCopy == null) {
|
if (retryHandlerCopy != null) {
|
||||||
retryHandlerCopy = DefaultHttpRequestRetryHandler.INSTANCE;
|
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;
|
HttpRoutePlanner routePlannerCopy = this.routePlanner;
|
||||||
|
|
|
@ -40,6 +40,7 @@ import java.util.Map;
|
||||||
import org.apache.hc.client5.http.AuthenticationStrategy;
|
import org.apache.hc.client5.http.AuthenticationStrategy;
|
||||||
import org.apache.hc.client5.http.ConnectionKeepAliveStrategy;
|
import org.apache.hc.client5.http.ConnectionKeepAliveStrategy;
|
||||||
import org.apache.hc.client5.http.HttpRequestRetryHandler;
|
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.SchemePortResolver;
|
||||||
import org.apache.hc.client5.http.ServiceUnavailableRetryStrategy;
|
import org.apache.hc.client5.http.ServiceUnavailableRetryStrategy;
|
||||||
import org.apache.hc.client5.http.SystemDefaultDnsResolver;
|
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.CookieSpecSupport;
|
||||||
import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy;
|
import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy;
|
||||||
import org.apache.hc.client5.http.impl.DefaultConnectionKeepAliveStrategy;
|
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.DefaultRedirectStrategy;
|
||||||
import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
|
import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
|
||||||
import org.apache.hc.client5.http.impl.DefaultUserTokenHandler;
|
import org.apache.hc.client5.http.impl.DefaultUserTokenHandler;
|
||||||
|
@ -204,6 +205,7 @@ public class HttpClientBuilder {
|
||||||
private LinkedList<ExecInterceptorEntry> execInterceptors;
|
private LinkedList<ExecInterceptorEntry> execInterceptors;
|
||||||
|
|
||||||
private HttpRequestRetryHandler retryHandler;
|
private HttpRequestRetryHandler retryHandler;
|
||||||
|
private HttpRequestRetryStrategy retryStrategy;
|
||||||
private HttpRoutePlanner routePlanner;
|
private HttpRoutePlanner routePlanner;
|
||||||
private RedirectStrategy redirectStrategy;
|
private RedirectStrategy redirectStrategy;
|
||||||
private ConnectionBackoffStrategy connectionBackoffStrategy;
|
private ConnectionBackoffStrategy connectionBackoffStrategy;
|
||||||
|
@ -505,6 +507,17 @@ public class HttpClientBuilder {
|
||||||
return this;
|
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.
|
* Disables automatic request recovery and re-execution.
|
||||||
*/
|
*/
|
||||||
|
@ -859,13 +872,21 @@ public class HttpClientBuilder {
|
||||||
|
|
||||||
// Add request retry executor, if not disabled
|
// Add request retry executor, if not disabled
|
||||||
if (!automaticRetriesDisabled) {
|
if (!automaticRetriesDisabled) {
|
||||||
HttpRequestRetryHandler retryHandlerCopy = this.retryHandler;
|
// This needs to be cleaned up as soon as HttpRequestRetryHandler will be removed
|
||||||
if (retryHandlerCopy == null) {
|
final HttpRequestRetryHandler retryHandlerCopy = this.retryHandler;
|
||||||
retryHandlerCopy = DefaultHttpRequestRetryHandler.INSTANCE;
|
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;
|
HttpRoutePlanner routePlannerCopy = this.routePlanner;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue