This is an automated email from the ASF dual-hosted git repository.
amagyar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git
The following commit(s) were added to refs/heads/master by this push:
new 7ac870f16 KNOX-2736 Knox clients should support retry/failover (#568)
7ac870f16 is described below
commit 7ac870f16563eda499b7eafa072bb119f4cd5754
Author: Attila Magyar <[email protected]>
AuthorDate: Mon May 16 10:35:44 2022 +0200
KNOX-2736 Knox clients should support retry/failover (#568)
---
.../apache/knox/gateway/shell/ClientContext.java | 33 ++++++
.../knox/gateway/shell/KnoxClientRetryHandler.java | 56 +++++++++++
.../org/apache/knox/gateway/shell/KnoxSession.java | 112 +++++++++++----------
.../apache/knox/gateway/shell/KnoxSessionTest.java | 30 ++++++
4 files changed, 179 insertions(+), 52 deletions(-)
diff --git
a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/ClientContext.java
b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/ClientContext.java
index b003a588d..84c5000e4 100644
---
a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/ClientContext.java
+++
b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/ClientContext.java
@@ -195,6 +195,39 @@ public class ClientContext {
public String endpointPublicCertPem() {
return configuration.getString("endpointPublicCertPem");
}
+
+ /**
+ * Number of retries, after a failure, before giving up.
+ */
+ public ConnectionContext retryCount(int retryCount) {
+ configuration.addProperty("retryCount", retryCount);
+ return this;
+ }
+
+ public int retryCount() {
+ return configuration.getInt("retryCount", 3);
+ }
+
+ /**
+ * true if it's OK to retry requests that have been sent
+ */
+ public ConnectionContext withRequestSentRetryEnabled(boolean
retryNonIdempotent) {
+ configuration.addProperty("requestSentRetryEnabled", retryNonIdempotent);
+ return this;
+ }
+
+ public boolean requestSentRetryEnabled() {
+ return configuration.getBoolean("requestSentRetryEnabled", false);
+ }
+
+ public ConnectionContext withRetryIntervalMillis(int msec) {
+ configuration.addProperty("retryIntervalMillis", msec);
+ return this;
+ }
+
+ public int retryIntervalMillis() {
+ return configuration.getInt("retryIntervalMillis", 1000);
+ }
}
/**
diff --git
a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxClientRetryHandler.java
b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxClientRetryHandler.java
new file mode 100644
index 000000000..6f8784091
--- /dev/null
+++
b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxClientRetryHandler.java
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+package org.apache.knox.gateway.shell;
+
+import java.io.InterruptedIOException;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.net.ssl.SSLException;
+
+import org.apache.http.HttpRequest;
+import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
+
+/**
+ * This is the same as StandardHttpRequestRetryHandler, but ConnectException
is not marked as non-retriable,
+ * so connection timeouts and connection refused are also retried
+ */
+public class KnoxClientRetryHandler extends DefaultHttpRequestRetryHandler {
+ private final Map<String, Boolean> idempotentMethods;
+
+ public KnoxClientRetryHandler(int retryCount, boolean
requestSentRetryEnabled) {
+ super(retryCount, requestSentRetryEnabled,
Arrays.asList(InterruptedIOException.class, UnknownHostException.class,
SSLException.class));
+ this.idempotentMethods = new ConcurrentHashMap();
+ this.idempotentMethods.put("GET", Boolean.TRUE);
+ this.idempotentMethods.put("HEAD", Boolean.TRUE);
+ this.idempotentMethods.put("PUT", Boolean.TRUE);
+ this.idempotentMethods.put("DELETE", Boolean.TRUE);
+ this.idempotentMethods.put("OPTIONS", Boolean.TRUE);
+ this.idempotentMethods.put("TRACE", Boolean.TRUE);
+ }
+
+ @Override
+ protected boolean handleAsIdempotent(HttpRequest request) {
+ String method =
request.getRequestLine().getMethod().toUpperCase(Locale.ROOT);
+ Boolean b = this.idempotentMethods.get(method);
+ return b != null && b;
+ }
+}
diff --git
a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxSession.java
b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxSession.java
index a3d691db3..b6a25762d 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxSession.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxSession.java
@@ -17,11 +17,55 @@
*/
package org.apache.knox.gateway.shell;
+import java.io.ByteArrayInputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Constructor;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.channels.FileChannel;
+import java.nio.channels.OverlappingFileLockException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
+import java.security.AccessController;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Principal;
+import java.security.PrivilegedAction;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.security.auth.Subject;
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.security.auth.callback.TextCallbackHandler;
-
+import de.thetaphi.forbiddenapis.SuppressForbidden;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
@@ -50,6 +94,8 @@ import org.apache.http.impl.auth.SPNegoSchemeFactory;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.DefaultServiceUnavailableRetryStrategy;
+import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.protocol.BasicHttpContext;
@@ -58,52 +104,6 @@ import
org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.shell.util.ClientTrustStoreHelper;
import org.apache.knox.gateway.util.JsonUtils;
-import javax.net.ssl.HostnameVerifier;
-import javax.net.ssl.SSLContext;
-import javax.security.auth.Subject;
-import javax.security.auth.login.AppConfigurationEntry;
-import javax.security.auth.login.Configuration;
-import javax.security.auth.login.LoginContext;
-import javax.security.auth.login.LoginException;
-
-import java.io.ByteArrayInputStream;
-import java.io.Closeable;
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.lang.reflect.Constructor;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.nio.channels.FileChannel;
-import java.nio.channels.OverlappingFileLockException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.StandardOpenOption;
-import java.security.AccessController;
-import java.security.GeneralSecurityException;
-import java.security.KeyStore;
-import java.security.KeyStoreException;
-import java.security.NoSuchAlgorithmException;
-import java.security.Principal;
-import java.security.PrivilegedAction;
-import java.security.cert.CertificateException;
-import java.security.cert.CertificateFactory;
-import java.security.cert.X509Certificate;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import de.thetaphi.forbiddenapis.SuppressForbidden;
-
public class KnoxSession implements Closeable {
private static final String DEFAULT_JAAS_FILE = "/jaas.conf";
public static final String JGSS_LOGIN_MOUDLE =
"com.sun.security.jgss.initiate";
@@ -369,12 +369,20 @@ public class KnoxSession implements Closeable {
new
UsernamePasswordCredentials(clientContext.username(),
clientContext.password()));
}
- return HttpClients.custom()
- .setConnectionManager(connectionManager)
- .setDefaultCredentialsProvider(credentialsProvider)
- .build();
+ HttpClientBuilder httpClientBuilder = HttpClients.custom()
+ .setConnectionManager(connectionManager)
+ .setDefaultCredentialsProvider(credentialsProvider);
+ if (clientContext.connection().retryCount() != -1) {
+ httpClientBuilder
+ .setRetryHandler(new KnoxClientRetryHandler(
+ clientContext.connection().retryCount(),
+ clientContext.connection().requestSentRetryEnabled()))
+ .setServiceUnavailableRetryStrategy(new
DefaultServiceUnavailableRetryStrategy(
+ clientContext.connection().retryCount(),
+ clientContext.connection().retryIntervalMillis()));
+ }
+ return httpClientBuilder.build();
}
-
}
protected X509Certificate generateCertificateFromBytes(byte[] certBytes)
throws CertificateException {
diff --git
a/gateway-shell/src/test/java/org/apache/knox/gateway/shell/KnoxSessionTest.java
b/gateway-shell/src/test/java/org/apache/knox/gateway/shell/KnoxSessionTest.java
index e408ff786..3a4a2d1fa 100644
---
a/gateway-shell/src/test/java/org/apache/knox/gateway/shell/KnoxSessionTest.java
+++
b/gateway-shell/src/test/java/org/apache/knox/gateway/shell/KnoxSessionTest.java
@@ -24,10 +24,14 @@ import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.bootstrap.HttpServer;
+import org.apache.http.impl.bootstrap.ServerBootstrap;
import org.junit.Test;
import javax.security.auth.Subject;
import java.io.IOException;
+import java.net.ServerSocket;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.List;
@@ -175,6 +179,32 @@ public class KnoxSessionTest {
}
}
+ @Test
+ public void testRetry() throws Exception {
+ int[] counter = new int[]{ 0 };
+ HttpServer server = ServerBootstrap.bootstrap()
+ .setListenerPort(findFreePort())
+ .registerHandler("/retry", (req, resp, ctx) ->
resp.setStatusCode(counter[0]++ < 2 ? 503 : 200))
+ .create();
+ server.start();
+ try {
+ ClientContext context = ClientContext.with("http://localhost:" +
server.getLocalPort());
+ context.connection().retryCount(2);
+ KnoxSession session = KnoxSession.login(context);
+ assertEquals(200, session.executeNow(new
HttpGet("/retry")).getStatusLine().getStatusCode());
+ session.close();
+ assertEquals(3, counter[0]);
+ } finally {
+ server.stop();
+ }
+ }
+
+ public static int findFreePort() throws IOException {
+ try(ServerSocket socket = new ServerSocket(0)) {
+ return socket.getLocalPort();
+ }
+ }
+
private static class LogHandler extends Handler {
List<String> logMessages = new ArrayList<>();