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<>();

Reply via email to