diff --git a/TESTING.asciidoc b/TESTING.asciidoc
index b62461d8b2b..bb3d20a2db4 100644
--- a/TESTING.asciidoc
+++ b/TESTING.asciidoc
@@ -184,7 +184,8 @@ The following are the options supported by the REST tests runner:
* `tests.rest[true|false|host:port]`: determines whether the REST tests need
to be run and if so whether to rely on an external cluster (providing host
-and port) or fire a test cluster (default)
+and port) or fire a test cluster (default). It's possible to provide a
+comma separated list of addresses to send requests in a round-robin fashion.
* `tests.rest.suite`: comma separated paths of the test suites to be run
(by default loaded from /rest-api-spec/test). It is possible to run only a subset
of the tests providing a sub-folder or even a single yaml file (the default
diff --git a/src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java b/src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java
index b914cb5e33b..004f3edea1d 100644
--- a/src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java
+++ b/src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java
@@ -850,6 +850,16 @@ public abstract class ElasticsearchIntegrationTest extends ElasticsearchTestCase
return annotation == null ? -1 : annotation.numNodes();
}
+ private int getMinNumNodes() {
+ ClusterScope annotation = getAnnotation(this.getClass());
+ return annotation == null ? TestCluster.DEFAULT_MIN_NUM_NODES : annotation.minNumNodes();
+ }
+
+ private int getMaxNumNodes() {
+ ClusterScope annotation = getAnnotation(this.getClass());
+ return annotation == null ? TestCluster.DEFAULT_MAX_NUM_NODES : annotation.maxNumNodes();
+ }
+
/**
* This method is used to obtain settings for the Nth node in the cluster.
* Nodes in this cluster are associated with an ordinal number such that nodes can
@@ -880,7 +890,15 @@ public abstract class ElasticsearchIntegrationTest extends ElasticsearchTestCase
};
}
- return new TestCluster(currentClusterSeed, numNodes, clusterName(scope.name(), ElasticsearchTestCase.CHILD_VM_ID, currentClusterSeed), nodeSettingsSource);
+ int minNumNodes, maxNumNodes;
+ if (numNodes >= 0) {
+ minNumNodes = maxNumNodes = numNodes;
+ } else {
+ minNumNodes = getMinNumNodes();
+ maxNumNodes = getMaxNumNodes();
+ }
+
+ return new TestCluster(currentClusterSeed, minNumNodes, maxNumNodes, clusterName(scope.name(), ElasticsearchTestCase.CHILD_VM_ID, currentClusterSeed), nodeSettingsSource);
}
/**
@@ -898,10 +916,23 @@ public abstract class ElasticsearchIntegrationTest extends ElasticsearchTestCase
/**
* Returns the number of nodes in the cluster. Default is -1 which means
- * a random number of nodes but at least 2
is used./
+ * a random number of nodes is used, where the minimum and maximum number of nodes
+ * are either the specified ones or the default ones if not specified.
*/
int numNodes() default -1;
+ /**
+ * Returns the minimum number of nodes in the cluster. Default is {@link TestCluster#DEFAULT_MIN_NUM_NODES}.
+ * Ignored when {@link ClusterScope#numNodes()} is set.
+ */
+ int minNumNodes() default TestCluster.DEFAULT_MIN_NUM_NODES;
+
+ /**
+ * Returns the maximum number of nodes in the cluster. Default is {@link TestCluster#DEFAULT_MAX_NUM_NODES}.
+ * Ignored when {@link ClusterScope#numNodes()} is set.
+ */
+ int maxNumNodes() default TestCluster.DEFAULT_MAX_NUM_NODES;
+
/**
* Returns the transport client ratio. By default this returns -1
which means a random
* ratio in the interval [0..1]
is used.
diff --git a/src/test/java/org/elasticsearch/test/TestCluster.java b/src/test/java/org/elasticsearch/test/TestCluster.java
index 8c7f2d154f4..845edba0e1e 100644
--- a/src/test/java/org/elasticsearch/test/TestCluster.java
+++ b/src/test/java/org/elasticsearch/test/TestCluster.java
@@ -104,7 +104,7 @@ public final class TestCluster implements Iterable {
public static final String TESTS_ENABLE_MOCK_MODULES = "tests.enable_mock_modules";
/**
- * A node level setting that holds a per node random seed that is consistent across node restarts
+ * A node level setting that holds a per node random seed that is consistent across node restarts
*/
public static final String SETTING_CLUSTER_NODE_SEED = "test.cluster.node.seed";
@@ -112,6 +112,10 @@ public final class TestCluster implements Iterable {
private static final boolean ENABLE_MOCK_MODULES = systemPropertyAsBoolean(TESTS_ENABLE_MOCK_MODULES, true);
+ static final int DEFAULT_MIN_NUM_NODES = 2;
+
+ static final int DEFAULT_MAX_NUM_NODES = 6;
+
private static long clusterSeed() {
String property = System.getProperty(TESTS_CLUSTER_SEED);
if (!Strings.hasLength(property)) {
@@ -135,9 +139,6 @@ public final class TestCluster implements Iterable {
private AtomicInteger nextNodeId = new AtomicInteger(0);
- /* We have a fixed number of shared nodes that we keep around across tests */
- private final int numSharedNodes;
-
/* Each shared node has a node seed that is used to start up the node and get default settings
* this is important if a node is randomly shut down in a test since the next test relies on a
* fully shared cluster to be more reproducible */
@@ -147,18 +148,34 @@ public final class TestCluster implements Iterable {
private final NodeSettingsSource nodeSettingsSource;
- TestCluster(long clusterSeed, String clusterName) {
- this(clusterSeed, -1, clusterName, NodeSettingsSource.EMPTY);
+ public TestCluster(long clusterSeed, String clusterName) {
+ this(clusterSeed, DEFAULT_MIN_NUM_NODES, DEFAULT_MAX_NUM_NODES, clusterName, NodeSettingsSource.EMPTY);
}
- public TestCluster(long clusterSeed, int numNodes, String clusterName) {
- this(clusterSeed, numNodes, clusterName, NodeSettingsSource.EMPTY);
+ public TestCluster(long clusterSeed, int minNumNodes, int maxNumNodes, String clusterName) {
+ this(clusterSeed, minNumNodes, maxNumNodes, clusterName, NodeSettingsSource.EMPTY);
}
- TestCluster(long clusterSeed, int numNodes, String clusterName, NodeSettingsSource nodeSettingsSource) {
+ public TestCluster(long clusterSeed, int minNumNodes, int maxNumNodes, String clusterName, NodeSettingsSource nodeSettingsSource) {
this.clusterName = clusterName;
+
+ if (minNumNodes < 0 || maxNumNodes < 0) {
+ throw new IllegalArgumentException("minimum and maximum number of nodes must be >= 0");
+ }
+
+ if (maxNumNodes < minNumNodes) {
+ throw new IllegalArgumentException("maximum number of nodes must be >= minimum number of nodes");
+ }
+
Random random = new Random(clusterSeed);
- numSharedNodes = numNodes == -1 ? 2 + random.nextInt(4) : numNodes; // at least 2 nodes if randomized
+
+ int numSharedNodes;
+ if (minNumNodes == maxNumNodes) {
+ numSharedNodes = minNumNodes;
+ } else {
+ numSharedNodes = minNumNodes + random.nextInt(maxNumNodes - minNumNodes);
+ }
+
assert numSharedNodes >= 0;
/*
* TODO
@@ -193,7 +210,7 @@ public final class TestCluster implements Iterable {
private static boolean isLocalTransportConfigured() {
if ("local".equals(System.getProperty("es.node.mode", "network"))) {
- return true;
+ return true;
}
return Boolean.parseBoolean(System.getProperty("es.node.local", "false"));
}
@@ -204,7 +221,7 @@ public final class TestCluster implements Iterable {
Settings settings = nodeSettingsSource.settings(nodeOrdinal);
if (settings != null) {
if (settings.get(CLUSTER_NAME_KEY) != null) {
- throw new ElasticsearchIllegalStateException("Tests must not set a '"+CLUSTER_NAME_KEY+"' as a node setting set '" + CLUSTER_NAME_KEY + "': [" + settings.get(CLUSTER_NAME_KEY) + "]");
+ throw new ElasticsearchIllegalStateException("Tests must not set a '" + CLUSTER_NAME_KEY + "' as a node setting set '" + CLUSTER_NAME_KEY + "': [" + settings.get(CLUSTER_NAME_KEY) + "]");
}
builder.put(settings);
}
@@ -220,7 +237,7 @@ public final class TestCluster implements Iterable {
Builder builder = ImmutableSettings.settingsBuilder()
/* use RAM directories in 10% of the runs */
//.put("index.store.type", random.nextInt(10) == 0 ? MockRamIndexStoreModule.class.getName() : MockFSIndexStoreModule.class.getName())
- // decrease the routing schedule so new nodes will be added quickly - some random value between 30 and 80 ms
+ // decrease the routing schedule so new nodes will be added quickly - some random value between 30 and 80 ms
.put("cluster.routing.schedule", (30 + random.nextInt(50)) + "ms")
// default to non gateway
.put("gateway.type", "none")
@@ -575,8 +592,7 @@ public final class TestCluster implements Iterable {
node = (InternalNode) nodeBuilder().settings(node.settings()).settings(newSettings).node();
resetClient();
}
-
-
+
@Override
public void close() {
@@ -865,7 +881,7 @@ public final class TestCluster implements Iterable {
nodeAndClient.restart(callback);
}
}
-
+
private void restartAllNodes(boolean rollingRestart, RestartCallback callback) throws Exception {
ensureOpen();
List toRemove = new ArrayList();
@@ -938,7 +954,7 @@ public final class TestCluster implements Iterable {
public void fullRestart(RestartCallback function) throws Exception {
restartAllNodes(false, function);
}
-
+
private String getMasterName() {
try {
@@ -1085,7 +1101,7 @@ public final class TestCluster implements Iterable {
@Override
public boolean apply(Settings settings) {
return nodeNames.contains(settings.get("name"));
-
+
}
}
diff --git a/src/test/java/org/elasticsearch/test/rest/RestTestExecutionContext.java b/src/test/java/org/elasticsearch/test/rest/RestTestExecutionContext.java
index 54e60460853..0f5ec14796d 100644
--- a/src/test/java/org/elasticsearch/test/rest/RestTestExecutionContext.java
+++ b/src/test/java/org/elasticsearch/test/rest/RestTestExecutionContext.java
@@ -29,6 +29,7 @@ import org.elasticsearch.test.rest.spec.RestSpec;
import java.io.Closeable;
import java.io.IOException;
+import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.Map;
@@ -50,9 +51,8 @@ public class RestTestExecutionContext implements Closeable {
private RestResponse response;
- public RestTestExecutionContext(String host, int port, RestSpec restSpec) throws RestException, IOException {
-
- this.restClient = new RestClient(host, port, restSpec);
+ public RestTestExecutionContext(InetSocketAddress[] addresses, RestSpec restSpec) throws RestException, IOException {
+ this.restClient = new RestClient(addresses, restSpec);
this.esVersion = restClient.getEsVersion();
}
diff --git a/src/test/java/org/elasticsearch/test/rest/client/RestClient.java b/src/test/java/org/elasticsearch/test/rest/client/RestClient.java
index c18ef40b886..d038b1407f9 100644
--- a/src/test/java/org/elasticsearch/test/rest/client/RestClient.java
+++ b/src/test/java/org/elasticsearch/test/rest/client/RestClient.java
@@ -33,6 +33,7 @@ import org.elasticsearch.test.rest.spec.RestSpec;
import java.io.Closeable;
import java.io.IOException;
+import java.net.InetSocketAddress;
import java.util.List;
import java.util.Map;
@@ -47,35 +48,47 @@ public class RestClient implements Closeable {
private final RestSpec restSpec;
private final CloseableHttpClient httpClient;
- private final String host;
- private final int port;
+ private final InetSocketAddress[] addresses;
private final String esVersion;
- public RestClient(String host, int port, RestSpec restSpec) throws IOException, RestException {
+ public RestClient(InetSocketAddress[] addresses, RestSpec restSpec) throws IOException, RestException {
+ assert addresses.length > 0;
this.restSpec = restSpec;
this.httpClient = createHttpClient();
- this.host = host;
- this.port = port;
- this.esVersion = readVersion();
- logger.info("REST client initialized [{}:{}], elasticsearch version: [{}]", host, port, esVersion);
+ this.addresses = addresses;
+ this.esVersion = readAndCheckVersion();
+ logger.info("REST client initialized {}, elasticsearch version: [{}]", addresses, esVersion);
}
- private String readVersion() throws IOException, RestException {
+ private String readAndCheckVersion() throws IOException, RestException {
//we make a manual call here without using callApi method, mainly because we are initializing
//and the randomized context doesn't exist for the current thread (would be used to choose the method otherwise)
RestApi restApi = restApi("info");
assert restApi.getPaths().size() == 1;
assert restApi.getMethods().size() == 1;
- RestResponse restResponse = new RestResponse(httpRequestBuilder()
- .path(restApi.getPaths().get(0))
- .method(restApi.getMethods().get(0)).execute());
- checkStatusCode(restResponse);
- Object version = restResponse.evaluate("version.number");
- if (version == null) {
- throw new RuntimeException("elasticsearch version not found in the response");
+
+ String version = null;
+ for (InetSocketAddress address : addresses) {
+ RestResponse restResponse = new RestResponse(new HttpRequestBuilder(httpClient)
+ .host(address.getHostName()).port(address.getPort())
+ .path(restApi.getPaths().get(0))
+ .method(restApi.getMethods().get(0)).execute());
+ checkStatusCode(restResponse);
+
+ Object latestVersion = restResponse.evaluate("version.number");
+ if (latestVersion == null) {
+ throw new RuntimeException("elasticsearch version not found in the response");
+ }
+ if (version == null) {
+ version = latestVersion.toString();
+ } else {
+ if (!latestVersion.equals(version)) {
+ throw new IllegalArgumentException("provided nodes addresses run different elasticsearch versions");
+ }
+ }
}
- return version.toString();
+ return version;
}
public String getEsVersion() {
@@ -208,7 +221,9 @@ public class RestClient implements Closeable {
}
protected HttpRequestBuilder httpRequestBuilder() {
- return new HttpRequestBuilder(httpClient).host(host).port(port);
+ //the address used is randomized between the available ones
+ InetSocketAddress address = RandomizedTest.randomFrom(addresses);
+ return new HttpRequestBuilder(httpClient).host(address.getHostName()).port(address.getPort());
}
protected CloseableHttpClient createHttpClient() {
diff --git a/src/test/java/org/elasticsearch/test/rest/junit/RestTestSuiteRunner.java b/src/test/java/org/elasticsearch/test/rest/junit/RestTestSuiteRunner.java
index a3321b44c97..cd1456b8d67 100644
--- a/src/test/java/org/elasticsearch/test/rest/junit/RestTestSuiteRunner.java
+++ b/src/test/java/org/elasticsearch/test/rest/junit/RestTestSuiteRunner.java
@@ -51,6 +51,7 @@ import org.junit.runners.model.Statement;
import java.io.File;
import java.io.IOException;
+import java.net.InetSocketAddress;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
@@ -181,35 +182,35 @@ public class RestTestSuiteRunner extends ParentRunner {
this.testSectionRandomnessOverride = randomnessOverride;
logger.info("Master seed: {}", SeedUtils.formatSeed(initialSeed));
- String host;
- int port;
+ List addresses = Lists.newArrayList();
if (runMode == RunMode.TEST_CLUSTER) {
- this.testCluster = new TestCluster(SHARED_CLUSTER_SEED, 1, clusterName("REST-tests", ElasticsearchTestCase.CHILD_VM_ID, SHARED_CLUSTER_SEED));
+ this.testCluster = new TestCluster(SHARED_CLUSTER_SEED, 1, 3,
+ clusterName("REST-tests", ElasticsearchTestCase.CHILD_VM_ID, SHARED_CLUSTER_SEED));
this.testCluster.beforeTest(runnerRandomness.getRandom(), 0.0f);
- HttpServerTransport httpServerTransport = testCluster.getInstance(HttpServerTransport.class);
- InetSocketTransportAddress inetSocketTransportAddress = (InetSocketTransportAddress) httpServerTransport.boundAddress().publishAddress();
- host = inetSocketTransportAddress.address().getHostName();
- port = inetSocketTransportAddress.address().getPort();
+ for (HttpServerTransport httpServerTransport : testCluster.getInstances(HttpServerTransport.class)) {
+ addresses.add(((InetSocketTransportAddress) httpServerTransport.boundAddress().publishAddress()).address());
+ }
} else {
this.testCluster = null;
String testsMode = System.getProperty(REST_TESTS_MODE);
- String[] split = testsMode.split(":");
- if (split.length < 2) {
- throw new InitializationError("address [" + testsMode + "] not valid");
- }
- host = split[0];
- try {
- port = Integer.valueOf(split[1]);
- } catch(NumberFormatException e) {
- throw new InitializationError("port is not valid, expected number but was [" + split[1] + "]");
+ String[] stringAddresses = testsMode.split(",");
+ for (String stringAddress : stringAddresses) {
+ String[] split = stringAddress.split(":");
+ if (split.length < 2) {
+ throw new InitializationError("address [" + testsMode + "] not valid");
+ }
+ try {
+ addresses.add(new InetSocketAddress(split[0], Integer.valueOf(split[1])));
+ } catch(NumberFormatException e) {
+ throw new InitializationError("port is not valid, expected number but was [" + split[1] + "]");
+ }
}
}
try {
String[] specPaths = resolvePathsProperty(REST_TESTS_SPEC, DEFAULT_SPEC_PATH);
RestSpec restSpec = RestSpec.parseFrom(DEFAULT_SPEC_PATH, specPaths);
-
- this.restTestExecutionContext = new RestTestExecutionContext(host, port, restSpec);
+ this.restTestExecutionContext = new RestTestExecutionContext(addresses.toArray(new InetSocketAddress[addresses.size()]), restSpec);
this.rootDescription = createRootDescription(getRootSuiteTitle());
this.restTestCandidates = collectTestCandidates(rootDescription);
} catch (InitializationError e) {
diff --git a/src/test/java/org/elasticsearch/tribe/TribeTests.java b/src/test/java/org/elasticsearch/tribe/TribeTests.java
index ecb6dc2575a..c47d62f523c 100644
--- a/src/test/java/org/elasticsearch/tribe/TribeTests.java
+++ b/src/test/java/org/elasticsearch/tribe/TribeTests.java
@@ -52,7 +52,7 @@ public class TribeTests extends ElasticsearchIntegrationTest {
@Before
public void setupSecondCluster() {
// create another cluster
- cluster2 = new TestCluster(randomLong(), 2, cluster().getClusterName() + "-2");
+ cluster2 = new TestCluster(randomLong(), 2, 2, cluster().getClusterName() + "-2");
cluster2.beforeTest(getRandom(), getPerTestTransportClientRatio());
cluster2.ensureAtLeastNumNodes(2);