Skip to main content
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--org.eclipse.scout.rt.server.test/src/test/java/org/eclipse/scout/rt/server/commons/http/HttpRetryTest.java5
-rw-r--r--org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/http/ApacheHttpTransportFactory.java37
-rw-r--r--org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/http/HttpConfigurationProperties.java70
-rw-r--r--org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/http/retry/CustomHttpRequestRetryHandler.java137
-rw-r--r--org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/http/retry/OneTimeRepeatableRequestEntityProxy.java105
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();
+ }
+}

Back to the top