blob: 9a2cb559377a1e3707ad9de86de74ac811eba049 [file] [log] [blame]
/*
* 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.solr.cloud.rule;
import java.lang.invoke.MethodHandles;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.lucene.util.LuceneTestCase;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.embedded.JettySolrRunner;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.client.solrj.request.GenericSolrRequest;
import org.apache.solr.client.solrj.response.SimpleSolrResponse;
import org.apache.solr.cloud.CloudTestUtils.AutoScalingRequest;
import org.apache.solr.cloud.SolrCloudTestCase;
import org.apache.solr.common.cloud.DocCollection;
import org.apache.solr.common.cloud.Replica;
import org.apache.solr.common.cloud.Slice;
import org.apache.solr.common.cloud.ZkStateReader;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.solr.client.solrj.SolrRequest.METHOD.GET;
import static org.apache.solr.client.solrj.SolrRequest.METHOD.POST;
import static org.apache.solr.common.params.CommonParams.COLLECTIONS_HANDLER_PATH;
import static org.junit.matchers.JUnitMatchers.containsString;
@LuceneTestCase.Slow
public class RulesTest extends SolrCloudTestCase {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@BeforeClass
public static void setupCluster() throws Exception {
System.setProperty("metricsEnabled", "true");
configureCluster(5)
.addConfig("conf", configset("cloud-minimal"))
.configure();
}
@org.junit.Rule
public ExpectedException expectedException = ExpectedException.none();
@After
public void removeCollections() throws Exception {
cluster.deleteAllCollections();
// clear any cluster policy test methods may have set
cluster.getSolrClient().getZkStateReader().getZkClient().setData(ZkStateReader.SOLR_AUTOSCALING_CONF_PATH,
"{}".getBytes(StandardCharsets.UTF_8), true);
}
@Test
public void doIntegrationTest() throws Exception {
assertEquals("Sanity Check: someone changed the cluster; " +
"test logic requires specific number of jetty nodes",
5, cluster.getJettySolrRunners().size());
final long minGB = (random().nextBoolean() ? 1 : 0);
final Path toTest = Paths.get("").toAbsolutePath();
assumeTrue("doIntegrationTest needs minGB="+minGB+" usable disk space",
ImplicitSnitch.getUsableSpaceInGB(toTest) > minGB);
String rulesColl = "rulesColl";
CollectionAdminRequest.createCollectionWithImplicitRouter(rulesColl, "conf", "shard1", 2)
.setRule("cores:<4", "node:*,replica:<2", "freedisk:>"+minGB)
.setSnitch("class:ImplicitSnitch")
.process(cluster.getSolrClient());
cluster.waitForActiveCollection(rulesColl, 1, 2);
DocCollection rulesCollection = getCollectionState(rulesColl);
@SuppressWarnings({"rawtypes"})
List list = (List) rulesCollection.get("rule");
assertEquals(3, list.size());
assertEquals ( "<4", ((Map)list.get(0)).get("cores"));
assertEquals("<2", ((Map) list.get(1)).get("replica"));
assertEquals(">"+minGB, ((Map) list.get(2)).get("freedisk"));
list = (List) rulesCollection.get("snitch");
assertEquals(1, list.size());
assertEquals ( "ImplicitSnitch", ((Map)list.get(0)).get("class"));
CollectionAdminRequest.createShard(rulesColl, "shard2").process(cluster.getSolrClient());
CollectionAdminRequest.addReplicaToShard(rulesColl, "shard2").process(cluster.getSolrClient());
waitForState("Should have found shard1 w/2active replicas + shard2 w/1active replica",
rulesColl, (liveNodes, collection) -> {
// short circut if collection is deleted
// or we don't yet have the correct number of slices
if (null == collection || 2 != collection.getSlices().size()) {
return false;
}
final Set<String> replicaNodes = new HashSet<>();
for (Slice slice : collection.getSlices()) {
// short circut if our slice isn't active
if (Slice.State.ACTIVE != slice.getState()) {
return false;
}
if (slice.getName().equals("shard1")) {
// for shard1, we should have 2 fully live replicas
final List<Replica> liveReplicas = slice.getReplicas
((r) -> r.isActive(liveNodes));
if (2 != liveReplicas.size()) {
return false;
}
replicaNodes.addAll(liveReplicas.stream().map
(Replica::getNodeName).collect(Collectors.toList()));
} else if (slice.getName().equals("shard2")) {
// for shard2, we should have 3 fully live replicas
final List<Replica> liveReplicas = slice.getReplicas
((r) -> r.isActive(liveNodes));
if (3 != liveReplicas.size()) {
return false;
}
replicaNodes.addAll(liveReplicas.stream().map
(Replica::getNodeName).collect(Collectors.toList()));
} else {
// WTF?
return false;
}
}
// now sanity check that the rules were *obeyed* and
// each replica is on a unique node
return 5 == replicaNodes.size();
});
// adding an additional replica should fail since our rule says at most one replica
// per node, and we know every node already has one replica
expectedException.expect(HttpSolrClient.RemoteSolrException.class);
expectedException.expectMessage(containsString("current number of eligible live nodes 0"));
CollectionAdminRequest.addReplicaToShard(rulesColl, "shard2").process(cluster.getSolrClient());
}
@Test
public void testPortRuleInPresenceOfClusterPolicy() throws Exception {
JettySolrRunner jetty = cluster.getRandomJetty(random());
String port = Integer.toString(jetty.getLocalPort());
// this cluster policy prohibits having any replicas on a node with the above port
String setClusterPolicyCommand = "{" +
" 'set-cluster-policy': [" +
" {'replica': 0, 'port':'" + port + "'}" +
" ]" +
"}";
@SuppressWarnings({"rawtypes"})
SolrRequest req = AutoScalingRequest.create(SolrRequest.METHOD.POST, setClusterPolicyCommand);
cluster.getSolrClient().request(req);
// but this collection is created with a replica placement rule that says all replicas must be created
// on a node with above port (in direct conflict with the cluster policy)
String rulesColl = "portRuleColl2";
CollectionAdminRequest.createCollectionWithImplicitRouter(rulesColl, "conf", "shard1", 2)
.setRule("port:" + port)
.setSnitch("class:ImplicitSnitch")
.process(cluster.getSolrClient());
waitForState("Collection should have followed port rule w/ImplicitSnitch, not cluster policy",
rulesColl, (liveNodes, rulesCollection) -> {
// first sanity check that the collection exists & the rules/snitch are listed
if (null == rulesCollection) {
return false;
} else {
@SuppressWarnings({"rawtypes"})
List list = (List) rulesCollection.get("rule");
if (null == list || 1 != list.size()) {
return false;
}
if (! port.equals(((Map) list.get(0)).get("port"))) {
return false;
}
list = (List) rulesCollection.get("snitch");
if (null == list || 1 != list.size()) {
return false;
}
if (! "ImplicitSnitch".equals(((Map)list.get(0)).get("class"))) {
return false;
}
}
if (2 != rulesCollection.getReplicas().size()) {
return false;
}
// now sanity check that the rules were *obeyed*
// (and the contradictory policy was ignored)
return rulesCollection.getReplicas().stream().allMatch
(replica -> (replica.getNodeName().contains(port) &&
replica.isActive(liveNodes)));
});
}
@Test
public void testPortRule() throws Exception {
JettySolrRunner jetty = cluster.getRandomJetty(random());
String port = Integer.toString(jetty.getLocalPort());
String rulesColl = "portRuleColl";
CollectionAdminRequest.createCollectionWithImplicitRouter(rulesColl, "conf", "shard1", 2)
.setRule("port:" + port)
.setSnitch("class:ImplicitSnitch")
.process(cluster.getSolrClient());
waitForState("Collection should have followed port rule w/ImplicitSnitch, not cluster policy",
rulesColl, (liveNodes, rulesCollection) -> {
// first sanity check that the collection exists & the rules/snitch are listed
if (null == rulesCollection) {
return false;
} else {
@SuppressWarnings({"rawtypes"})
List list = (List) rulesCollection.get("rule");
if (null == list || 1 != list.size()) {
return false;
}
if (! port.equals(((Map) list.get(0)).get("port"))) {
return false;
}
list = (List) rulesCollection.get("snitch");
if (null == list || 1 != list.size()) {
return false;
}
if (! "ImplicitSnitch".equals(((Map)list.get(0)).get("class"))) {
return false;
}
}
if (2 != rulesCollection.getReplicas().size()) {
return false;
}
// now sanity check that the rules were *obeyed*
return rulesCollection.getReplicas().stream().allMatch
(replica -> (replica.getNodeName().contains(port) &&
replica.isActive(liveNodes)));
});
}
@Test
@SuppressWarnings({"unchecked"})
public void testHostFragmentRule() throws Exception {
String rulesColl = "hostFragment";
JettySolrRunner jetty = cluster.getRandomJetty(random());
String host = jetty.getBaseUrl().getHost();
String[] ipFragments = host.split("\\.");
String ip_1 = ipFragments[ipFragments.length - 1];
String ip_2 = ipFragments[ipFragments.length - 2];
CollectionAdminRequest.createCollectionWithImplicitRouter(rulesColl, "conf", "shard1", 2)
.setRule("ip_2:" + ip_2, "ip_1:" + ip_1)
.setSnitch("class:ImplicitSnitch")
.process(cluster.getSolrClient());
cluster.waitForActiveCollection(rulesColl, 1, 2);
DocCollection rulesCollection = getCollectionState(rulesColl);
@SuppressWarnings({"rawtypes"})
List<Map> list = (List<Map>) rulesCollection.get("rule");
assertEquals(2, list.size());
assertEquals(ip_2, list.get(0).get("ip_2"));
assertEquals(ip_1, list.get(1).get("ip_1"));
list = (List) rulesCollection.get("snitch");
assertEquals(1, list.size());
assertEquals("ImplicitSnitch", list.get(0).get("class"));
}
@Test
public void testHostFragmentRuleThrowsExceptionWhenIpDoesNotMatch() throws Exception {
String rulesColl = "ipRuleColl";
JettySolrRunner jetty = cluster.getRandomJetty(random());
String host = jetty.getBaseUrl().getHost();
String[] ipFragments = host.split("\\.");
String ip_1 = ipFragments[ipFragments.length - 1];
String ip_2 = ipFragments[ipFragments.length - 2];
expectedException.expect(HttpSolrClient.RemoteSolrException.class);
expectedException.expectMessage(containsString("ip_1"));
CollectionAdminRequest.createCollectionWithImplicitRouter(rulesColl, "conf", "shard1", 2)
.setRule("ip_2:" + ip_2, "ip_1:" + ip_1 + "9999")
.setSnitch("class:ImplicitSnitch")
.process(cluster.getSolrClient());
}
@Test
public void testInvokeApi() throws Exception {
JettySolrRunner jetty = cluster.getRandomJetty(random());
try (SolrClient client = getHttpSolrClient(jetty.getBaseUrl().toString())) {
GenericSolrRequest req = new GenericSolrRequest(GET, "/____v2/node/invoke", new ModifiableSolrParams()
.add("class", ImplicitSnitch.class.getName())
.add("cores", "1")
.add("freedisk", "1")
);
SimpleSolrResponse rsp = req.process(client);
assertNotNull(((Map) rsp.getResponse().get(ImplicitSnitch.class.getName())).get("cores"));
assertNotNull(((Map) rsp.getResponse().get(ImplicitSnitch.class.getName())).get("freedisk"));
}
}
@Test
public void testModifyColl() throws Exception {
final Path toTest = Paths.get("").toAbsolutePath();
final long minGB1 = (random().nextBoolean() ? 1 : 0);
final long minGB2 = 5;
assumeTrue("testModifyColl needs minGB1="+minGB1+" usable disk space",
ImplicitSnitch.getUsableSpaceInGB(toTest) > minGB1);
assumeTrue("testModifyColl needs minGB2="+minGB2+" usable disk space",
ImplicitSnitch.getUsableSpaceInGB(toTest) > minGB2);
String rulesColl = "modifyColl";
CollectionAdminRequest.createCollection(rulesColl, "conf", 1, 2)
.setRule("cores:<4", "node:*,replica:1", "freedisk:>" + minGB1)
.setSnitch("class:ImplicitSnitch")
.process(cluster.getSolrClient());
cluster.waitForActiveCollection(rulesColl, 1, 2);
// TODO: Make a MODIFYCOLLECTION SolrJ class
ModifiableSolrParams p = new ModifiableSolrParams();
p.add("collection", rulesColl);
p.add("action", "MODIFYCOLLECTION");
p.add("rule", "cores:<5");
p.add("rule", "node:*,replica:1");
p.add("rule", "freedisk:>"+minGB2);
p.add("autoAddReplicas", "true");
cluster.getSolrClient().request(new GenericSolrRequest(POST, COLLECTIONS_HANDLER_PATH, p));
waitForState("Should have found updated rules in DocCollection",
rulesColl, (liveNodes, rulesCollection) -> {
if (null == rulesCollection) {
return false;
}
@SuppressWarnings({"rawtypes"})
List list = (List) rulesCollection.get("rule");
if (null == list || 3 != list.size()) {
return false;
}
if (! "<5".equals(((Map) list.get(0)).get("cores"))) {
return false;
}
if (! "1".equals(((Map) list.get(1)).get("replica"))) {
return false;
}
if (! (">"+minGB2).equals(((Map) list.get(2)).get("freedisk"))) {
return false;
}
if (! "true".equals(String.valueOf(rulesCollection.getProperties().get("autoAddReplicas")))) {
return false;
}
list = (List) rulesCollection.get("snitch");
if (null == list || 1 != list.size()) {
return false;
}
if (! "ImplicitSnitch".equals(((Map) list.get(0)).get("class"))) {
return false;
}
return true;
});
}
}