diff options
5 files changed, 348 insertions, 6 deletions
diff --git a/org.eclipse.scout.rt.server.test/src/test/java/org/eclipse/scout/rt/server/commons/http/HttpRetryTest.java b/org.eclipse.scout.rt.server.test/src/test/java/org/eclipse/scout/rt/server/commons/http/HttpRetryTest.java index 9b383954c4..8f2bbb03b9 100644 --- a/org.eclipse.scout.rt.server.test/src/test/java/org/eclipse/scout/rt/server/commons/http/HttpRetryTest.java +++ b/org.eclipse.scout.rt.server.test/src/test/java/org/eclipse/scout/rt/server/commons/http/HttpRetryTest.java @@ -259,8 +259,9 @@ public class HttpRetryTest { //emulate a socket close before data is received AtomicInteger count = new AtomicInteger(1); m_server.withChannelInterceptor((channel, superCall) -> { - if (count.getAndIncrement() < 2) { - channel.getHttpTransport().abort(new SocketException("TEST:cannot write")); + //2 failures in a row, the first would have been retried by the CustomHttpRequestRetryHandler + if (count.getAndIncrement() < 3) { + channel.getHttpTransport().abort(new IOException("TEST:cannot write")); return; } superCall.call(); diff --git a/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/http/ApacheHttpTransportFactory.java b/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/http/ApacheHttpTransportFactory.java index 0eb8bee972..acf55b7e22 100644 --- a/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/http/ApacheHttpTransportFactory.java +++ b/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/http/ApacheHttpTransportFactory.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2010-2017 BSI Business Systems Integration AG. + * Copyright (c) 2010-2019 BSI Business Systems Integration AG. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -35,7 +35,10 @@ import org.eclipse.scout.rt.shared.http.HttpConfigurationProperties.ApacheHttpTr import org.eclipse.scout.rt.shared.http.HttpConfigurationProperties.ApacheHttpTransportKeepAliveProperty; import org.eclipse.scout.rt.shared.http.HttpConfigurationProperties.ApacheHttpTransportMaxConnectionsPerRouteProperty; import org.eclipse.scout.rt.shared.http.HttpConfigurationProperties.ApacheHttpTransportMaxConnectionsTotalProperty; +import org.eclipse.scout.rt.shared.http.HttpConfigurationProperties.ApacheHttpTransportRetryOnNoHttpResponseExceptionProperty; +import org.eclipse.scout.rt.shared.http.HttpConfigurationProperties.ApacheHttpTransportRetryOnSocketExceptionByConnectionResetProperty; import org.eclipse.scout.rt.shared.http.proxy.ConfigurableProxySelector; +import org.eclipse.scout.rt.shared.http.retry.CustomHttpRequestRetryHandler; import org.eclipse.scout.rt.shared.http.transport.ApacheHttpTransport; import org.eclipse.scout.rt.shared.servicetunnel.http.MultiSessionCookieStore; @@ -70,6 +73,14 @@ public class ApacheHttpTransportFactory implements IHttpTransportFactory { * @param builder */ protected void setConnectionKeepAliveAndRetrySettings(HttpClientBuilder builder) { + addConnectionKeepAliveSettings(builder); + addRetrySettings(builder); + } + + /** + * @param builder + */ + protected void addConnectionKeepAliveSettings(HttpClientBuilder builder) { final boolean keepAliveProp = CONFIG.getPropertyValue(ApacheHttpTransportKeepAliveProperty.class); if (keepAliveProp) { builder.setConnectionReuseStrategy(DefaultClientConnectionReuseStrategy.INSTANCE); @@ -77,7 +88,29 @@ public class ApacheHttpTransportFactory implements IHttpTransportFactory { else { builder.setConnectionReuseStrategy(NoConnectionReuseStrategy.INSTANCE); } - builder.setRetryHandler(new DefaultHttpRequestRetryHandler(1, false)); + } + + /** + * @param builder + */ + protected void addRetrySettings(HttpClientBuilder builder) { + final boolean retryOnNoHttpResponseException = CONFIG.getPropertyValue(ApacheHttpTransportRetryOnNoHttpResponseExceptionProperty.class); + final boolean retryOnSocketExceptionByConnectionReset = CONFIG.getPropertyValue(ApacheHttpTransportRetryOnSocketExceptionByConnectionResetProperty.class); + if (retryOnNoHttpResponseException || retryOnSocketExceptionByConnectionReset) { + builder.setRetryHandler(new CustomHttpRequestRetryHandler(1, false, retryOnNoHttpResponseException, retryOnSocketExceptionByConnectionReset)); + } + else { + builder.setRetryHandler(new DefaultHttpRequestRetryHandler(1, false)); + } + } + + /** + * @deprecated use {@link #createHttpClientConnectionManager()} + * @return + */ + @Deprecated + protected HttpClientConnectionManager getConfiguredConnectionManager() { + return createHttpClientConnectionManager(); } /** diff --git a/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/http/HttpConfigurationProperties.java b/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/http/HttpConfigurationProperties.java index a878947f7e..fd94c13c61 100644 --- a/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/http/HttpConfigurationProperties.java +++ b/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/http/HttpConfigurationProperties.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2010-2018 BSI Business Systems Integration AG. + * Copyright (c) 2010-2019 BSI Business Systems Integration AG. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -10,6 +10,9 @@ ******************************************************************************/ package org.eclipse.scout.rt.shared.http; +import java.net.SocketException; + +import org.apache.http.NoHttpResponseException; import org.eclipse.scout.rt.platform.config.AbstractBooleanConfigProperty; import org.eclipse.scout.rt.platform.config.AbstractIntegerConfigProperty; @@ -27,7 +30,7 @@ public final class HttpConfigurationProperties { @Override public String description() { - return "Specifies the maximum life time in milliseconds for kept alive connections of the Apache HTTP client. The defautl value is 1 hour."; + return "Specifies the maximum life time in milliseconds for kept alive connections of the Apache HTTP client. The default value is 1 hour."; } @Override @@ -94,4 +97,67 @@ public final class HttpConfigurationProperties { return "scout.http.keepAlive"; } } + + /** + * Enable retry of request (includes non-idempotent requests) on {@link NoHttpResponseException} + * <p> + * Assuming that the cause of the exception was most probably a stale socket channel on the server side. + * <p> + * For apache tomcat see http://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html#d5e659 + * + * @since 7.0 + */ + public static class ApacheHttpTransportRetryOnNoHttpResponseExceptionProperty extends AbstractBooleanConfigProperty { + + @Override + public Boolean getDefaultValue() { + return true; + } + + @Override + @SuppressWarnings("findbugs:VA_FORMAT_STRING_USES_NEWLINE") + public String description() { + return "Enable retry of request (includes non-idempotent requests) on NoHttpResponseException\n" + + "Assuming that the cause of the exception was most probably a stale socket channel on the server side.\n" + + "For apache tomcat see http://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html#d5e659\n" + + "The default value is true"; + } + + @Override + public String getKey() { + return "scout.http.retryOnNoHttpResponseException"; + } + } + + /** + * Enable retry of request (includes non-idempotent requests) on {@link SocketException} with message "Connection + * reset" + * <p> + * Assuming that the cause of the exception was most probably a stale socket channel on the server side. + * <p> + * For apache tomcat see http://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html#d5e659 + * + * @since 7.0 + */ + public static class ApacheHttpTransportRetryOnSocketExceptionByConnectionResetProperty extends AbstractBooleanConfigProperty { + + @Override + public Boolean getDefaultValue() { + return true; + } + + @Override + @SuppressWarnings("findbugs:VA_FORMAT_STRING_USES_NEWLINE") + public String description() { + return "Enable retry of request (includes non-idempotent requests) on {@link SocketException} with message 'Connection reset'\n" + + "Assuming that the cause of the exception was most probably a stale socket channel on the server side.\n" + + "For apache tomcat see http://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html#d5e659\n" + + "The default value is true"; + } + + @Override + public String getKey() { + return "scout.http.retryOnSocketExceptionByConnectionReset"; + } + } } diff --git a/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/http/retry/CustomHttpRequestRetryHandler.java b/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/http/retry/CustomHttpRequestRetryHandler.java new file mode 100644 index 0000000000..60ee913e86 --- /dev/null +++ b/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/http/retry/CustomHttpRequestRetryHandler.java @@ -0,0 +1,137 @@ +/******************************************************************************* + * Copyright (c) 2019 BSI Business Systems Integration AG. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * BSI Business Systems Integration AG - initial API and implementation + ******************************************************************************/ +package org.eclipse.scout.rt.shared.http.retry; + +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.HashSet; +import java.util.Set; + +import javax.net.ssl.SSLException; + +import org.apache.http.HttpRequest; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; +import org.apache.http.protocol.HttpContext; +import org.apache.http.util.Args; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Fix for issue 'NoHttpResponseException: localhost failed to respond' on stale socket channels + * + * @since 7.0 + */ +public class CustomHttpRequestRetryHandler extends DefaultHttpRequestRetryHandler { + private static final Logger LOG = LoggerFactory.getLogger(CustomHttpRequestRetryHandler.class); + + private final Set<Class<? extends IOException>> m_nonRetriableClasses; + private final boolean m_retryOnNoHttpResponseException; + private final boolean m_retryOnSocketExceptionByConnectionReset; + + public CustomHttpRequestRetryHandler(final int retryCount, final boolean requestSentRetryEnabled, final boolean retryOnNoHttpResponseException, final boolean retryOnSocketExceptionByConnectionReset) { + this( + retryCount, + requestSentRetryEnabled, Arrays.asList( + InterruptedIOException.class, + UnknownHostException.class, + ConnectException.class, + SSLException.class), + retryOnNoHttpResponseException, + retryOnSocketExceptionByConnectionReset); + } + + protected CustomHttpRequestRetryHandler(int retryCount, boolean requestSentRetryEnabled, Collection<Class<? extends IOException>> clazzes, final boolean retryOnNoHttpResponseException, + final boolean retryOnSocketExceptionByConnectionReset) { + super(retryCount, requestSentRetryEnabled, clazzes); + m_nonRetriableClasses = new HashSet<>(clazzes); + m_retryOnNoHttpResponseException = retryOnNoHttpResponseException; + m_retryOnSocketExceptionByConnectionReset = retryOnSocketExceptionByConnectionReset; + } + + @Override + @SuppressWarnings("deprecation") + public boolean retryRequest( + final IOException exception, + final int executionCount, + final HttpContext context) { + Args.notNull(exception, "Exception parameter"); + Args.notNull(context, "HTTP context"); + if (executionCount > getRetryCount()) { + // Do not retry if over max retry count + return false; + } + if (m_nonRetriableClasses.contains(exception.getClass())) { + return false; + } + else { + for (final Class<? extends IOException> rejectException : m_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() || isRequestSentRetryEnabled()) { + // Retry if the request has not been sent fully or + // if it's OK to retry methods that have been sent + return true; + } + + if (detectStaleSocketChannel(exception, clientContext)) { + return true; + } + + // otherwise do not retry + return false; + } + + /** + * Fix for NoHttpResponseException that can occur even if connection check is done in millisecond interval + * <p> + * http://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html#d5e659 + */ + protected boolean detectStaleSocketChannel(IOException exception, HttpContext context) { + boolean retry; + if (m_retryOnNoHttpResponseException && exception instanceof org.apache.http.NoHttpResponseException) { + LOG.warn("detected a 'NoHttpResponseException', assuming a stale socket channel; retry non-idempotent request"); + retry = true; + } + else if (m_retryOnSocketExceptionByConnectionReset && exception instanceof java.net.SocketException && "Connection reset".equals(exception.getMessage())) { + LOG.warn("detected a 'SocketException: Connection reset', assuming a stale socket channel; retry non-idempotent request"); + retry = true; + } + else { + retry = false; + } + + if (retry) { + HttpRequest request = HttpClientContext.adapt(context).getRequest(); + OneTimeRepeatableRequestEntityProxy.installRetry(request); + } + return retry; + } +} diff --git a/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/http/retry/OneTimeRepeatableRequestEntityProxy.java b/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/http/retry/OneTimeRepeatableRequestEntityProxy.java new file mode 100644 index 0000000000..b41ebec638 --- /dev/null +++ b/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/http/retry/OneTimeRepeatableRequestEntityProxy.java @@ -0,0 +1,105 @@ +/******************************************************************************* + * Copyright (c) 2019 BSI Business Systems Integration AG. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * BSI Business Systems Integration AG - initial API and implementation + ******************************************************************************/ +package org.eclipse.scout.rt.shared.http.retry; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpEntityEnclosingRequest; +import org.apache.http.HttpRequest; + +/** + * A Proxy class for {@link org.apache.http.HttpEntity} that supports retry regardless of the enclosed + * {@link HttpEntity#isRepeatable()} + * + * @since 7.0 + */ +public class OneTimeRepeatableRequestEntityProxy implements HttpEntity { + private final HttpEntity m_original; + private boolean m_consumed; + + public static void installRetry(HttpRequest request) { + if (request instanceof HttpEntityEnclosingRequest) { + final HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity(); + if (entity != null && !(entity instanceof OneTimeRepeatableRequestEntityProxy)) { + ((HttpEntityEnclosingRequest) request).setEntity(new OneTimeRepeatableRequestEntityProxy(entity)); + } + } + } + + public OneTimeRepeatableRequestEntityProxy(final HttpEntity original) { + m_original = original; + } + + public HttpEntity getOriginal() { + return m_original; + } + + @Override + public boolean isRepeatable() { + return !m_consumed; + } + + @Override + public boolean isChunked() { + return m_original.isChunked(); + } + + @Override + public long getContentLength() { + return m_original.getContentLength(); + } + + @Override + public Header getContentType() { + return m_original.getContentType(); + } + + @Override + public Header getContentEncoding() { + return m_original.getContentEncoding(); + } + + @Override + public InputStream getContent() throws IOException { + return m_original.getContent(); + } + + @Override + public void writeTo(final OutputStream outstream) throws IOException { + m_consumed = true; + m_original.writeTo(outstream); + } + + @Override + public boolean isStreaming() { + return m_original.isStreaming(); + } + + @SuppressWarnings("deprecation") + @Override + public void consumeContent() throws IOException { + m_consumed = true; + m_original.consumeContent(); + } + + @Override + public String toString() { + return new StringBuilder(getClass().getSimpleName()) + .append("{") + .append(m_original) + .append('}') + .toString(); + } +} |