HBASE-28500 Rest Java client library assumes stateless servers (#5804)

add a sticky flag which forces using the same server for all requests

Signed-off-by: Peter Somogyi <psomogyi@apache.org>
Signed-off-by: Duo Zhang <zhangduo@apache.org>
diff --git a/hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/client/Client.java b/hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/client/Client.java
index 9283cb9..96cb5f2 100644
--- a/hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/client/Client.java
+++ b/hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/client/Client.java
@@ -79,6 +79,8 @@
 
   private HttpClient httpClient;
   private Cluster cluster;
+  private Integer lastNodeId;
+  private boolean sticky = false;
   private Configuration conf;
   private boolean sslEnabled;
   private HttpResponse resp;
@@ -249,10 +251,12 @@
   }
 
   /**
-   * Execute a transaction method given only the path. Will select at random one of the members of
-   * the supplied cluster definition and iterate through the list until a transaction can be
-   * successfully completed. The definition of success here is a complete HTTP transaction,
-   * irrespective of result code.
+   * Execute a transaction method given only the path. If sticky is false: Will select at random one
+   * of the members of the supplied cluster definition and iterate through the list until a
+   * transaction can be successfully completed. The definition of success here is a complete HTTP
+   * transaction, irrespective of result code. If sticky is true: For the first request it will
+   * select a random one of the members of the supplied cluster definition. For subsequent requests
+   * it will use the same member, and it will not automatically re-try if the call fails.
    * @param cluster the cluster definition
    * @param method  the transaction method
    * @param headers HTTP header values to send
@@ -265,10 +269,12 @@
     if (cluster.nodes.size() < 1) {
       throw new IOException("Cluster is empty");
     }
-    int start = (int) Math.round((cluster.nodes.size() - 1) * Math.random());
-    int i = start;
+    if (lastNodeId == null || !sticky) {
+      lastNodeId = (int) (cluster.nodes.size() * Math.random());
+    }
+    int start = lastNodeId;
     do {
-      cluster.lastHost = cluster.nodes.get(i);
+      cluster.lastHost = cluster.nodes.get(lastNodeId);
       try {
         StringBuilder sb = new StringBuilder();
         if (sslEnabled) {
@@ -302,7 +308,11 @@
       } catch (URISyntaxException use) {
         lastException = new IOException(use);
       }
-    } while (++i != start && i < cluster.nodes.size());
+      if (!sticky) {
+        lastNodeId = (++lastNodeId) % cluster.nodes.size();
+      }
+      // Do not retry if sticky. Let the caller handle the error.
+    } while (!sticky && lastNodeId != start);
     throw lastException;
   }
 
@@ -407,6 +417,28 @@
   }
 
   /**
+   * The default behaviour is load balancing by sending each request to a random host. This DOES NOT
+   * work with scans, which have state on the REST servers. Make sure sticky is set to true before
+   * attempting Scan related operations if more than one host is defined in the cluster.
+   * @return whether subsequent requests will use the same host
+   */
+  public boolean isSticky() {
+    return sticky;
+  }
+
+  /**
+   * The default behaviour is load balancing by sending each request to a random host. This DOES NOT
+   * work with scans, which have state on the REST servers. Set sticky to true before attempting
+   * Scan related operations if more than one host is defined in the cluster. Nodes must not be
+   * added or removed from the Cluster object while sticky is true.
+   * @param sticky whether subsequent requests will use the same host
+   */
+  public void setSticky(boolean sticky) {
+    lastNodeId = null;
+    this.sticky = sticky;
+  }
+
+  /**
    * Send a HEAD request
    * @param path the path or URI
    * @return a Response object with response detail