You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by no...@apache.org on 2023/09/18 19:44:28 UTC

[solr] branch branch_9x updated: More test cases for Coordinator node role (#1782)

This is an automated email from the ASF dual-hosted git repository.

noble pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/branch_9x by this push:
     new 0cb27e96cfc More test cases for Coordinator node role (#1782)
0cb27e96cfc is described below

commit 0cb27e96cfcbe2de843b4f28dbefb1942fc2917c
Author: patsonluk <pa...@users.noreply.github.com>
AuthorDate: Mon Sep 18 12:41:08 2023 -0700

    More test cases for Coordinator node role (#1782)
---
 .../solr/configsets/cache-control/conf/schema.xml  |  27 +++
 .../configsets/cache-control/conf/solrconfig.xml   |  54 +++++
 .../apache/solr/search/TestCoordinatorRole.java    | 260 +++++++++++++++++++--
 3 files changed, 324 insertions(+), 17 deletions(-)

diff --git a/solr/core/src/test-files/solr/configsets/cache-control/conf/schema.xml b/solr/core/src/test-files/solr/configsets/cache-control/conf/schema.xml
new file mode 100644
index 00000000000..36d5cfd2588
--- /dev/null
+++ b/solr/core/src/test-files/solr/configsets/cache-control/conf/schema.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ 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.
+-->
+<schema name="minimal" version="1.1">
+    <fieldType name="string" class="solr.StrField"/>
+    <fieldType name="int" class="${solr.tests.IntegerFieldType}" docValues="${solr.tests.numeric.dv}" precisionStep="0" omitNorms="true" positionIncrementGap="0"/>
+    <fieldType name="long" class="${solr.tests.LongFieldType}" docValues="${solr.tests.numeric.dv}" precisionStep="0" omitNorms="true" positionIncrementGap="0"/>
+    <dynamicField name="*" type="string" indexed="true" stored="true"/>
+    <!-- for versioning -->
+    <field name="_version_" type="long" indexed="true" stored="true"/>
+    <field name="_root_" type="string" indexed="true" stored="true" multiValued="false" required="false"/>
+    <field name="id" type="string" indexed="true" stored="true"/>
+    <dynamicField name="*_s"  type="string"  indexed="true"  stored="true" />
+    <uniqueKey>id</uniqueKey>
+</schema>
\ No newline at end of file
diff --git a/solr/core/src/test-files/solr/configsets/cache-control/conf/solrconfig.xml b/solr/core/src/test-files/solr/configsets/cache-control/conf/solrconfig.xml
new file mode 100644
index 00000000000..bd27a88952a
--- /dev/null
+++ b/solr/core/src/test-files/solr/configsets/cache-control/conf/solrconfig.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" ?>
+
+<!--
+ 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.
+-->
+
+<!-- Minimal solrconfig.xml with /select, /admin and /update only -->
+
+<config>
+
+    <dataDir>${solr.data.dir:}</dataDir>
+
+    <directoryFactory name="DirectoryFactory"
+                      class="${solr.directoryFactory:solr.NRTCachingDirectoryFactory}"/>
+    <schemaFactory class="ClassicIndexSchemaFactory"/>
+
+    <luceneMatchVersion>${tests.luceneMatchVersion:LATEST}</luceneMatchVersion>
+
+    <updateHandler class="solr.DirectUpdateHandler2">
+        <commitWithin>
+            <softCommit>${solr.commitwithin.softcommit:true}</softCommit>
+        </commitWithin>
+        <updateLog class="${solr.ulog:solr.UpdateLog}"></updateLog>
+    </updateHandler>
+
+    <requestDispatcher>
+        <httpCaching>
+            <cacheControl>max-age=30, public</cacheControl>
+        </httpCaching>
+    </requestDispatcher>
+
+    <requestHandler name="/select" class="solr.SearchHandler">
+        <lst name="defaults">
+            <str name="echoParams">explicit</str>
+            <str name="indent">true</str>
+            <str name="df">text</str>
+        </lst>
+
+    </requestHandler>
+    <indexConfig>
+        <mergeScheduler class="${solr.mscheduler:org.apache.lucene.index.ConcurrentMergeScheduler}"/>
+        :  </indexConfig>
+</config>
\ No newline at end of file
diff --git a/solr/core/src/test/org/apache/solr/search/TestCoordinatorRole.java b/solr/core/src/test/org/apache/solr/search/TestCoordinatorRole.java
index 538c6b44703..581f048785d 100644
--- a/solr/core/src/test/org/apache/solr/search/TestCoordinatorRole.java
+++ b/solr/core/src/test/org/apache/solr/search/TestCoordinatorRole.java
@@ -21,6 +21,8 @@ import static org.apache.solr.common.params.CommonParams.OMIT_HEADER;
 import static org.apache.solr.common.params.CommonParams.TRUE;
 
 import java.lang.invoke.MethodHandles;
+import java.net.HttpURLConnection;
+import java.net.URL;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.EnumSet;
@@ -51,6 +53,7 @@ import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrInputDocument;
 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.cloud.ZkStateReaderAccessor;
 import org.apache.solr.common.params.CommonParams;
@@ -585,18 +588,70 @@ public class TestCoordinatorRole extends SolrCloudTestCase {
     }
   }
 
+  public void testConfigset() throws Exception {
+    final int DATA_NODE_COUNT = 1;
+    MiniSolrCloudCluster cluster =
+        configureCluster(DATA_NODE_COUNT)
+            .addConfig("conf1", configset("cloud-minimal"))
+            .addConfig("conf2", configset("cache-control"))
+            .configure();
+
+    List<String> dataNodes =
+        cluster.getJettySolrRunners().stream()
+            .map(JettySolrRunner::getNodeName)
+            .collect(Collectors.toUnmodifiableList());
+
+    try {
+      CollectionAdminRequest.createCollection("c1", "conf1", 2, 1).process(cluster.getSolrClient());
+      cluster.waitForActiveCollection("c1", 2, 2);
+      CollectionAdminRequest.createCollection("c2", "conf2", 2, 1).process(cluster.getSolrClient());
+      cluster.waitForActiveCollection("c2", 2, 2);
+
+      System.setProperty(NodeRoles.NODE_ROLES_PROP, "coordinator:on");
+      JettySolrRunner coordinatorJetty;
+      try {
+        coordinatorJetty = cluster.startJettySolrRunner();
+      } finally {
+        System.clearProperty(NodeRoles.NODE_ROLES_PROP);
+      }
+
+      // Tricky to test configset, since operation such as collection status would direct it to the
+      // OS node.
+      // So we use query and check the cache response header which is determined by the
+      // solr-config.xml in the configset
+      // However using solr client would drop cache respons header hence we need to use plain java
+      // HttpURLConnection
+      URL url = new URL(coordinatorJetty.getBaseUrl() + "/c1/select?q=*:*");
+      HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
+      urlConnection.connect();
+
+      // conf1 has no cache-control
+      assertNull(urlConnection.getHeaderField("cache-control"));
+
+      url = new URL(coordinatorJetty.getBaseUrl() + "/c2/select?q=*:*");
+      urlConnection = (HttpURLConnection) url.openConnection();
+      urlConnection.connect();
+
+      // conf2 has cache-control defined
+      assertTrue(urlConnection.getHeaderField("cache-control").contains("max-age=30"));
+    } finally {
+      cluster.shutdown();
+    }
+  }
+
   public void testWatch() throws Exception {
-    final int DATA_NODE_COUNT = 2;
+    final int DATA_NODE_COUNT = 1;
     MiniSolrCloudCluster cluster =
         configureCluster(DATA_NODE_COUNT)
             .addConfig("conf1", configset("cloud-minimal"))
             .configure();
-    final String TEST_COLLECTION = "c1";
+    final String TEST_COLLECTION_1 = "c1";
+    final String TEST_COLLECTION_2 = "c2";
 
     try {
       CloudSolrClient client = cluster.getSolrClient();
-      CollectionAdminRequest.createCollection(TEST_COLLECTION, "conf1", 1, 2).process(client);
-      cluster.waitForActiveCollection(TEST_COLLECTION, 1, 2);
+      CollectionAdminRequest.createCollection(TEST_COLLECTION_1, "conf1", 1, 2).process(client);
+      cluster.waitForActiveCollection(TEST_COLLECTION_1, 1, 2);
       System.setProperty(NodeRoles.NODE_ROLES_PROP, "coordinator:on");
       JettySolrRunner coordinatorJetty;
       try {
@@ -610,26 +665,37 @@ public class TestCoordinatorRole extends SolrCloudTestCase {
       ZkStateReaderAccessor zkWatchAccessor = new ZkStateReaderAccessor(zkStateReader);
 
       // no watch at first
-      assertTrue(!zkWatchAccessor.getWatchedCollections().contains(TEST_COLLECTION));
+      assertTrue(!zkWatchAccessor.getWatchedCollections().contains(TEST_COLLECTION_1));
       new QueryRequest(new SolrQuery("*:*"))
           .setPreferredNodes(List.of(coordinatorJetty.getNodeName()))
-          .process(client, TEST_COLLECTION); // ok no exception thrown
+          .process(client, TEST_COLLECTION_1); // ok no exception thrown
 
       // now it should be watching it after the query
-      assertTrue(zkWatchAccessor.getWatchedCollections().contains(TEST_COLLECTION));
+      assertTrue(zkWatchAccessor.getWatchedCollections().contains(TEST_COLLECTION_1));
+
+      // add another collection
+      CollectionAdminRequest.createCollection(TEST_COLLECTION_2, "conf1", 1, 2).process(client);
+      cluster.waitForActiveCollection(TEST_COLLECTION_2, 1, 2);
+      new QueryRequest(new SolrQuery("*:*"))
+          .setPreferredNodes(List.of(coordinatorJetty.getNodeName()))
+          .process(client, TEST_COLLECTION_2);
+      // watch both collections
+      assertTrue(zkWatchAccessor.getWatchedCollections().contains(TEST_COLLECTION_1));
+      assertTrue(zkWatchAccessor.getWatchedCollections().contains(TEST_COLLECTION_2));
 
-      CollectionAdminRequest.deleteReplica(TEST_COLLECTION, "shard1", 1).process(client);
-      cluster.waitForActiveCollection(TEST_COLLECTION, 1, 1);
+      CollectionAdminRequest.deleteReplica(TEST_COLLECTION_1, "shard1", 1).process(client);
+      cluster.waitForActiveCollection(TEST_COLLECTION_1, 1, 1);
       new QueryRequest(new SolrQuery("*:*"))
           .setPreferredNodes(List.of(coordinatorJetty.getNodeName()))
-          .process(client, TEST_COLLECTION); // ok no exception thrown
+          .process(client, TEST_COLLECTION_1); // ok no exception thrown
 
       // still one replica left, should not remove the watch
-      assertTrue(zkWatchAccessor.getWatchedCollections().contains(TEST_COLLECTION));
+      assertTrue(zkWatchAccessor.getWatchedCollections().contains(TEST_COLLECTION_1));
 
-      CollectionAdminRequest.deleteCollection(TEST_COLLECTION).process(client);
-      zkStateReader.waitForState(TEST_COLLECTION, 30, TimeUnit.SECONDS, Objects::isNull);
-      assertNull(zkStateReader.getCollection(TEST_COLLECTION)); // check the cluster state
+      // now delete c1 and ensure it's cleared from various logic
+      CollectionAdminRequest.deleteCollection(TEST_COLLECTION_1).process(client);
+      zkStateReader.waitForState(TEST_COLLECTION_1, 30, TimeUnit.SECONDS, Objects::isNull);
+      assertNull(zkStateReader.getCollection(TEST_COLLECTION_1)); // check the cluster state
 
       // ensure querying throws exception
       assertExceptionThrownWithMessageContaining(
@@ -638,10 +704,170 @@ public class TestCoordinatorRole extends SolrCloudTestCase {
           () ->
               new QueryRequest(new SolrQuery("*:*"))
                   .setPreferredNodes(List.of(coordinatorJetty.getNodeName()))
-                  .process(client, TEST_COLLECTION));
+                  .process(client, TEST_COLLECTION_1));
+
+      // watch should be removed after c1 deletion
+      assertTrue(!zkWatchAccessor.getWatchedCollections().contains(TEST_COLLECTION_1));
+      // still watching c2
+      assertTrue(zkWatchAccessor.getWatchedCollections().contains(TEST_COLLECTION_2));
+    } finally {
+      cluster.shutdown();
+    }
+  }
+
+  public void testSplitShard() throws Exception {
+    final int DATA_NODE_COUNT = 1;
+    MiniSolrCloudCluster cluster =
+        configureCluster(DATA_NODE_COUNT)
+            .addConfig("conf1", configset("cloud-minimal"))
+            .configure();
+
+    try {
+
+      final String COLLECTION_NAME = "c1";
+      CollectionAdminRequest.createCollection(COLLECTION_NAME, "conf1", 1, 1)
+          .process(cluster.getSolrClient());
+      cluster.waitForActiveCollection(COLLECTION_NAME, 1, 1);
+
+      int DOC_PER_COLLECTION_COUNT = 1000;
+      UpdateRequest ur = new UpdateRequest();
+      for (int i = 0; i < DOC_PER_COLLECTION_COUNT; i++) {
+        SolrInputDocument doc = new SolrInputDocument();
+        doc.addField("id", COLLECTION_NAME + "-" + i);
+        ur.add(doc);
+      }
+      CloudSolrClient client = cluster.getSolrClient();
+      ur.commit(client, COLLECTION_NAME);
+
+      System.setProperty(NodeRoles.NODE_ROLES_PROP, "coordinator:on");
+      JettySolrRunner coordinatorJetty;
+      try {
+        coordinatorJetty = cluster.startJettySolrRunner();
+      } finally {
+        System.clearProperty(NodeRoles.NODE_ROLES_PROP);
+      }
+
+      QueryResponse response =
+          new QueryRequest(new SolrQuery("*:*"))
+              .setPreferredNodes(List.of(coordinatorJetty.getNodeName()))
+              .process(client, COLLECTION_NAME);
+
+      assertEquals(DOC_PER_COLLECTION_COUNT, response.getResults().getNumFound());
+
+      // now split the shard
+      CollectionAdminRequest.splitShard(COLLECTION_NAME).setShardName("shard1").process(client);
+      waitForState(
+          "Failed to wait for child shards after split",
+          COLLECTION_NAME,
+          (liveNodes, collectionState) ->
+              collectionState.getSlice("shard1_0") != null
+                  && collectionState.getSlice("shard1_0").getState() == Slice.State.ACTIVE
+                  && collectionState.getSlice("shard1_1") != null
+                  && collectionState.getSlice("shard1_1").getState() == Slice.State.ACTIVE);
+
+      // delete the parent shard
+      CollectionAdminRequest.deleteShard(COLLECTION_NAME, "shard1").process(client);
+      waitForState(
+          "Parent shard is not yet deleted after split",
+          COLLECTION_NAME,
+          (liveNodes, collectionState) -> collectionState.getSlice("shard1") == null);
+
+      response =
+          new QueryRequest(new SolrQuery("*:*"))
+              .setPreferredNodes(List.of(coordinatorJetty.getNodeName()))
+              .process(client, COLLECTION_NAME);
+
+      assertEquals(DOC_PER_COLLECTION_COUNT, response.getResults().getNumFound());
+    } finally {
+      cluster.shutdown();
+    }
+  }
+
+  public void testMoveReplica() throws Exception {
+    final int DATA_NODE_COUNT = 2;
+    MiniSolrCloudCluster cluster =
+        configureCluster(DATA_NODE_COUNT)
+            .addConfig("conf1", configset("cloud-minimal"))
+            .configure();
+
+    List<String> dataNodes =
+        cluster.getJettySolrRunners().stream()
+            .map(JettySolrRunner::getNodeName)
+            .collect(Collectors.toUnmodifiableList());
+    try {
+
+      final String COLLECTION_NAME = "c1";
+      String fromNode = dataNodes.get(0); // put the shard on first data node
+      CollectionAdminRequest.createCollection(COLLECTION_NAME, "conf1", 1, 1)
+          .setCreateNodeSet(fromNode)
+          .process(cluster.getSolrClient());
+      // ensure replica is placed on the expected node
+      waitForState(
+          "Cannot find replica on first node yet",
+          COLLECTION_NAME,
+          (liveNodes, collectionState) -> {
+            if (collectionState.getReplicas().size() == 1) {
+              Replica replica = collectionState.getReplicas().get(0);
+              return fromNode.equals(replica.getNodeName())
+                  && replica.getState() == Replica.State.ACTIVE;
+            }
+            return false;
+          });
+
+      int DOC_PER_COLLECTION_COUNT = 1000;
+      UpdateRequest ur = new UpdateRequest();
+      for (int i = 0; i < DOC_PER_COLLECTION_COUNT; i++) {
+        SolrInputDocument doc = new SolrInputDocument();
+        doc.addField("id", COLLECTION_NAME + "-" + i);
+        ur.add(doc);
+      }
+      CloudSolrClient client = cluster.getSolrClient();
+      ur.commit(client, COLLECTION_NAME);
+
+      System.setProperty(NodeRoles.NODE_ROLES_PROP, "coordinator:on");
+      JettySolrRunner coordinatorJetty;
+      try {
+        coordinatorJetty = cluster.startJettySolrRunner();
+      } finally {
+        System.clearProperty(NodeRoles.NODE_ROLES_PROP);
+      }
+
+      QueryResponse response =
+          new QueryRequest(new SolrQuery("*:*"))
+              .setPreferredNodes(List.of(coordinatorJetty.getNodeName()))
+              .process(client, COLLECTION_NAME);
+
+      assertEquals(DOC_PER_COLLECTION_COUNT, response.getResults().getNumFound());
+
+      // now move the shard/replica
+      String replicaName = getCollectionState(COLLECTION_NAME).getReplicas().get(0).getName();
+      String toNodeName = dataNodes.get(1);
+      CollectionAdminRequest.moveReplica(COLLECTION_NAME, replicaName, toNodeName).process(client);
+      waitForState(
+          "Cannot find replica on second node yet after repliac move",
+          COLLECTION_NAME,
+          (liveNodes, collectionState) -> {
+            if (collectionState.getReplicas().size() == 1) {
+              Replica replica = collectionState.getReplicas().get(0);
+              return toNodeName.equals(replica.getNodeName())
+                  && replica.getState() == Replica.State.ACTIVE;
+            }
+            return false;
+          });
+
+      // We must stop the first node to ensure that query directs to the correct node from
+      // coordinator.
+      // In case if coordinator node has the wrong info (replica on first node), it might still
+      // return valid result if
+      // we do not stop the first node as first node might forward the query to second node.
+      cluster.getJettySolrRunners().get(0).stop();
+
+      response =
+          new QueryRequest(new SolrQuery("*:*"))
+              .setPreferredNodes(List.of(coordinatorJetty.getNodeName()))
+              .process(client, COLLECTION_NAME);
 
-      // watch should be removed after collection deletion
-      assertTrue(!zkWatchAccessor.getWatchedCollections().contains(TEST_COLLECTION));
+      assertEquals(DOC_PER_COLLECTION_COUNT, response.getResults().getNumFound());
     } finally {
       cluster.shutdown();
     }