在上一篇文章里我们介绍了 httpclient 连接池中空闲连接的清理,在这里我们主要介绍 http 连接的重试机制。
http连接的重试
httpclient 连接池也支持请求的重试,即在请求失败的情况下进行重试,对于重试设计以下几个关键点。
如何开启重试
如何定义重试次数
如何进行重试
如何开启重试
在 httpclient 连接池中,连接发送请求的重试是由 HttpRequestRetryHandler 类型的对象来处理,在HttpClientBuilder 有方法 disableAutomaticRetries() 来关闭重试,默认情况下重试是开启的,所以如果希望禁止重试那么就调用此方法。然后对于HttpClientBuilder 在构建 httpclient 的时候会根据这设置来确定 HttpRequestRetryHandler ,核心代码如下:
if (!automaticRetriesDisabled) {
HttpRequestRetryHandler retryHandlerCopy = this.retryHandler;
if (retryHandlerCopy == null) {
retryHandlerCopy = DefaultHttpRequestRetryHandler.INSTANCE;
}
execChain = new RetryExec(execChain, retryHandlerCopy);
}
httpclient 默认是开启重试机制的,如果希望关闭重试,则在构造中调用 HttpClientBuilder 的 disableAutomaticRetries() 方法。
构造过程中如果开启了重试,那么则设置对象实例 HttpRequestRetryHandler 来负责重试。
HttpRequestRetryHandler 可以通过 Builder 对象来在外部设置。
如果没有设置 HttpRequestRetryHandler 实例, 那么默认的为 DefaultHttpRequestRetryHandler 类型。
如何定义重试次数
如果使用默认重试机制,那么重试次数定义在 DefaultHttpRequestRetryHandler 对象实例中,其核心代码如下:
public static final DefaultHttpRequestRetryHandler INSTANCE = new DefaultHttpRequestRetryHandler();
public DefaultHttpRequestRetryHandler() {
this(3, false);
}
public DefaultHttpRequestRetryHandler(final int retryCount, final boolean requestSentRetryEnabled) {
this(retryCount, requestSentRetryEnabled, Arrays.asList(InterruptedIOException.class, UnknownHostException.class, ConnectException.class,SSLException.class));
}
protected DefaultHttpRequestRetryHandler(final int retryCount, final boolean requestSentRetryEnabled, final Collection<Class<? extends IOException>> clazzes) {
super();
this.retryCount = retryCount;
this.requestSentRetryEnabled = requestSentRetryEnabled;
this.nonRetriableClasses = new HashSet<Class<? extends IOException>>();
for (final Class<? extends IOException> clazz: clazzes) {
this.nonRetriableClasses.add(clazz);
}
}
在 DefaultHttpRequestRetryHandler 对象的默认构造函数中重试次数为 3。
同时定义发生什么类型的 Exception 不会进行重试,在默认构造函数中所定义的这些异常类型为InterruptedIOException/UnknownHostException/ConnectException/SSLExcetion 。
如何进行重试
重试的过程定义在 RetryExec 对象实例的 execute() 方法里,其核心代码为:
for (int execCount = 1;; execCount++) {
try {
return this.requestExecutor.execute(route, request, context, execAware);
} catch (final IOException ex) {
if (execAware != null && execAware.isAborted()) {
this.log.debug("Request has been aborted");
throw ex;
}
if (retryHandler.retryRequest(ex, execCount, context)) {
if (this.log.isInfoEnabled()) {
this.log.info("I/O exception ("+ ex.getClass().getName() + ") caught when processing request to "+ route +": "+ ex.getMessage());
}
if (this.log.isDebugEnabled()) {
this.log.debug(ex.getMessage(), ex);
}
if (!RequestEntityProxy.isRepeatable(request)) {
this.log.debug("Cannot retry non-repeatable request");
throw new NonRepeatableRequestException("Cannot retry request " + "with a non-repeatable request entity", ex);
}
request.setHeaders(origheaders);
if (this.log.isInfoEnabled()) {
this.log.info("Retrying request to " + route);
}
} else {
if (ex instanceof NoHttpResponseException) {
final NoHttpResponseException updatedex = new NoHttpResponseException(route.getTargetHost().toHostString() + " failed to respond");
updatedex.setStackTrace(ex.getStackTrace());
throw updatedex;
}
throw ex;
}
}
}
根据设置的重试次数进行循环发 http 请求,如果出现 IOException 则进行重试尝试。
如果请求 abort ,则取消重试。
对于请求 abort 的定义, 是指调了以前文章介绍的 ManagedHttpClientConnection 类型对象的shutdown 方法,绕过 TCP4 次握手关闭 socket 连接,直接设置 linger 通过发送 RST(reset) 包来快速关闭连接,核心代码如下:
public void shutdown() throws IOException {
final Socket socket = this.socketHolder.getAndSet(null);
if (socket != null) {
// force abortive close (RST)
try {
socket.setSoLinger(true, 0);
} catch (final IOException ex) {
// empty here
}
finally {
socket.close();
}
}
}
如果 retryHandler 决定重试,那么就进行下一次重试,然后重试次数减 1。
对于 DefaultHttpRequestRetryHandler 对象实例决定是否重试逻辑如下:
public boolean retryRequest(final IOException exception, final int executionCount, final HttpContext context) {
Args.notNull(exception, "Exception parameter");
Args.notNull(context, "HTTP context");
if (executionCount > this.retryCount) {
// Do not retry if over max retry count
return false;
}
if (this.nonRetriableClasses.contains(exception.getClass())) {
return false;
}
for (final Class<? extends IOException> rejectException : this.nonRetriableClasses) {
if (rejectException.isInstance(exception)) {
return false;
}
}
final HttpClientContext clientContext = HttpClientContext.adapt(context);
final HttpRequest request = clientContext.getRequest();
if(requestIsAborted(request)){
return false;
}
if (handleAsIdempotent(request)) {
// Retry if the request is considered idempotent
return true;
}
if (!clientContext.isRequestSent() || this.requestSentRetryEnabled) {
// Retry if the request has not been sent fully or
// if it's OK to retry methods that have been sent
return true;
}
// otherwise do not retry
return false;
}
如何到达重试次数则不重试。
如果请求 abort 则不重试。
如果发生的异常在 retryHanlder 不进行重试的异常名单里或者是名单里的实例,则不重试。在默认构造函数之中所定义的这些异常类型为InterruptedIOException/UnknownHostException/ConnectException/SSLExcetion 。
如果是 idempotent 幂等类型请求则重试, get 为幂等类型请求。
如果是请求没有发出去或者 retryHandler 的 requestSentRetryEnabled 属性为 true 则重试,该值默认为 false 。
除以上情况外的其他情况均不重试。
目前先写到这里,在下一篇文章里我们开始介绍 httpclient 连接池对于 ssl 支持。