Skip to content

Commit 3a1e267

Browse files
committed
Add test for low-level client round-robin behaviour (#31616)
1 parent e648edd commit 3a1e267

File tree

2 files changed

+75
-22
lines changed

2 files changed

+75
-22
lines changed

client/rest/src/main/java/org/elasticsearch/client/RestClient.java

+12-14
Original file line numberDiff line numberDiff line change
@@ -615,16 +615,16 @@ private void setHeaders(HttpRequest httpRequest, Collection<Header> requestHeade
615615
*/
616616
private NodeTuple<Iterator<Node>> nextNode() throws IOException {
617617
NodeTuple<List<Node>> nodeTuple = this.nodeTuple;
618-
List<Node> hosts = selectHosts(nodeTuple, blacklist, lastNodeIndex, nodeSelector);
618+
Iterable<Node> hosts = selectNodes(nodeTuple, blacklist, lastNodeIndex, nodeSelector);
619619
return new NodeTuple<>(hosts.iterator(), nodeTuple.authCache);
620620
}
621621

622622
/**
623-
* Select hosts to try. Package private for testing.
623+
* Select nodes to try and sorts them so that the first one will be tried initially, then the following ones
624+
* if the previous attempt failed and so on. Package private for testing.
624625
*/
625-
static List<Node> selectHosts(NodeTuple<List<Node>> nodeTuple,
626-
Map<HttpHost, DeadHostState> blacklist, AtomicInteger lastNodeIndex,
627-
NodeSelector nodeSelector) throws IOException {
626+
static Iterable<Node> selectNodes(NodeTuple<List<Node>> nodeTuple, Map<HttpHost, DeadHostState> blacklist,
627+
AtomicInteger lastNodeIndex, NodeSelector nodeSelector) throws IOException {
628628
/*
629629
* Sort the nodes into living and dead lists.
630630
*/
@@ -653,24 +653,22 @@ static List<Node> selectHosts(NodeTuple<List<Node>> nodeTuple,
653653
nodeSelector.select(selectedLivingNodes);
654654
if (false == selectedLivingNodes.isEmpty()) {
655655
/*
656-
* Rotate the list so subsequent requests will prefer the
657-
* nodes in a different order.
656+
* Rotate the list using a global counter as the distance so subsequent
657+
* requests will try the nodes in a different order.
658658
*/
659659
Collections.rotate(selectedLivingNodes, lastNodeIndex.getAndIncrement());
660660
return selectedLivingNodes;
661661
}
662662
}
663663

664664
/*
665-
* Last resort: If there are no good nodes to use, either because
665+
* Last resort: there are no good nodes to use, either because
666666
* the selector rejected all the living nodes or because there aren't
667667
* any living ones. Either way, we want to revive a single dead node
668-
* that the NodeSelectors are OK with. We do this by sorting the dead
669-
* nodes by their revival time and passing them through the
670-
* NodeSelector so it can have its say in which nodes are ok and their
671-
* ordering. If the selector is ok with any of the nodes then use just
672-
* the first one in the list because we only want to revive a single
673-
* node.
668+
* that the NodeSelectors are OK with. We do this by passing the dead
669+
* nodes through the NodeSelector so it can have its say in which nodes
670+
* are ok. If the selector is ok with any of the nodes then we will take
671+
* the one in the list that has the lowest revival time and try it.
674672
*/
675673
if (false == deadNodes.isEmpty()) {
676674
final List<DeadNode> selectedDeadNodes = new ArrayList<>(deadNodes);

client/rest/src/test/java/org/elasticsearch/client/RestClientTests.java

+63-8
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121

2222
import org.apache.http.Header;
2323
import org.apache.http.HttpHost;
24+
import org.apache.http.client.AuthCache;
25+
import org.apache.http.impl.auth.BasicScheme;
26+
import org.apache.http.impl.client.BasicAuthCache;
2427
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
2528
import org.elasticsearch.client.DeadHostStateTests.ConfigurableTimeSupplier;
2629
import org.elasticsearch.client.RestClient.NodeTuple;
@@ -35,13 +38,14 @@
3538
import java.util.List;
3639
import java.util.Map;
3740
import java.util.concurrent.CountDownLatch;
38-
import java.util.concurrent.atomic.AtomicInteger;
3941
import java.util.concurrent.TimeUnit;
42+
import java.util.concurrent.atomic.AtomicInteger;
4043

4144
import static java.util.Collections.singletonList;
4245
import static org.elasticsearch.client.RestClientTestUtil.getHttpMethods;
4346
import static org.hamcrest.Matchers.instanceOf;
4447
import static org.junit.Assert.assertEquals;
48+
import static org.junit.Assert.assertSame;
4549
import static org.junit.Assert.assertThat;
4650
import static org.junit.Assert.assertTrue;
4751
import static org.junit.Assert.fail;
@@ -407,8 +411,8 @@ public String toString() {
407411
* blacklist time. It'll revive the node that is closest
408412
* to being revived that the NodeSelector is ok with.
409413
*/
410-
assertEquals(singletonList(n1), RestClient.selectHosts(nodeTuple, blacklist, new AtomicInteger(), NodeSelector.ANY));
411-
assertEquals(singletonList(n2), RestClient.selectHosts(nodeTuple, blacklist, new AtomicInteger(), not1));
414+
assertEquals(singletonList(n1), RestClient.selectNodes(nodeTuple, blacklist, new AtomicInteger(), NodeSelector.ANY));
415+
assertEquals(singletonList(n2), RestClient.selectNodes(nodeTuple, blacklist, new AtomicInteger(), not1));
412416

413417
/*
414418
* Try a NodeSelector that excludes all nodes. This should
@@ -449,23 +453,23 @@ private void assertSelectLivingHosts(List<Node> expectedNodes, NodeTuple<List<No
449453
Map<HttpHost, DeadHostState> blacklist, NodeSelector nodeSelector) throws IOException {
450454
int iterations = 1000;
451455
AtomicInteger lastNodeIndex = new AtomicInteger(0);
452-
assertEquals(expectedNodes, RestClient.selectHosts(nodeTuple, blacklist, lastNodeIndex, nodeSelector));
456+
assertEquals(expectedNodes, RestClient.selectNodes(nodeTuple, blacklist, lastNodeIndex, nodeSelector));
453457
// Calling it again rotates the set of results
454458
for (int i = 1; i < iterations; i++) {
455459
Collections.rotate(expectedNodes, 1);
456460
assertEquals("iteration " + i, expectedNodes,
457-
RestClient.selectHosts(nodeTuple, blacklist, lastNodeIndex, nodeSelector));
461+
RestClient.selectNodes(nodeTuple, blacklist, lastNodeIndex, nodeSelector));
458462
}
459463
}
460464

461465
/**
462-
* Assert that {@link RestClient#selectHosts} fails on the provided arguments.
466+
* Assert that {@link RestClient#selectNodes} fails on the provided arguments.
463467
* @return the message in the exception thrown by the failure
464468
*/
465-
private String assertSelectAllRejected( NodeTuple<List<Node>> nodeTuple,
469+
private static String assertSelectAllRejected( NodeTuple<List<Node>> nodeTuple,
466470
Map<HttpHost, DeadHostState> blacklist, NodeSelector nodeSelector) {
467471
try {
468-
RestClient.selectHosts(nodeTuple, blacklist, new AtomicInteger(0), nodeSelector);
472+
RestClient.selectNodes(nodeTuple, blacklist, new AtomicInteger(0), nodeSelector);
469473
throw new AssertionError("expected selectHosts to fail");
470474
} catch (IOException e) {
471475
return e.getMessage();
@@ -478,5 +482,56 @@ private static RestClient createRestClient() {
478482
new Header[] {}, nodes, null, null, null);
479483
}
480484

485+
public void testRoundRobin() throws IOException {
486+
int numNodes = randomIntBetween(2, 10);
487+
AuthCache authCache = new BasicAuthCache();
488+
List<Node> nodes = new ArrayList<>(numNodes);
489+
for (int i = 0; i < numNodes; i++) {
490+
Node node = new Node(new HttpHost("localhost", 9200 + i));
491+
nodes.add(node);
492+
authCache.put(node.getHost(), new BasicScheme());
493+
}
494+
NodeTuple<List<Node>> nodeTuple = new NodeTuple<>(nodes, authCache);
495+
496+
//test the transition from negative to positive values
497+
AtomicInteger lastNodeIndex = new AtomicInteger(-numNodes);
498+
assertNodes(nodeTuple, lastNodeIndex, 50);
499+
assertEquals(-numNodes + 50, lastNodeIndex.get());
500+
501+
//test the highest positive values up to MAX_VALUE
502+
lastNodeIndex.set(Integer.MAX_VALUE - numNodes * 10);
503+
assertNodes(nodeTuple, lastNodeIndex, numNodes * 10);
504+
assertEquals(Integer.MAX_VALUE, lastNodeIndex.get());
505+
506+
//test the transition from MAX_VALUE to MIN_VALUE
507+
//this is the only time where there is most likely going to be a jump from a node
508+
//to another one that's not necessarily the next one.
509+
assertEquals(Integer.MIN_VALUE, lastNodeIndex.incrementAndGet());
510+
assertNodes(nodeTuple, lastNodeIndex, 50);
511+
assertEquals(Integer.MIN_VALUE + 50, lastNodeIndex.get());
512+
}
481513

514+
private static void assertNodes(NodeTuple<List<Node>> nodeTuple, AtomicInteger lastNodeIndex, int runs) throws IOException {
515+
int distance = lastNodeIndex.get() % nodeTuple.nodes.size();
516+
/*
517+
* Collections.rotate is not super intuitive: distance 1 means that the last element will become the first and so on,
518+
* while distance -1 means that the second element will become the first and so on.
519+
*/
520+
int expectedOffset = distance > 0 ? nodeTuple.nodes.size() - distance : Math.abs(distance);
521+
for (int i = 0; i < runs; i++) {
522+
Iterable<Node> selectedNodes = RestClient.selectNodes(nodeTuple, Collections.<HttpHost, DeadHostState>emptyMap(),
523+
lastNodeIndex, NodeSelector.ANY);
524+
List<Node> expectedNodes = nodeTuple.nodes;
525+
int index = 0;
526+
for (Node actualNode : selectedNodes) {
527+
Node expectedNode = expectedNodes.get((index + expectedOffset) % expectedNodes.size());
528+
assertSame(expectedNode, actualNode);
529+
index++;
530+
}
531+
expectedOffset--;
532+
if (expectedOffset < 0) {
533+
expectedOffset += nodeTuple.nodes.size();
534+
}
535+
}
536+
}
482537
}

0 commit comments

Comments
 (0)