You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ds...@apache.org on 2023/05/30 20:50:44 UTC
[solr] branch main updated: SOLR-14630: SolrJ queries: form the URL to the replica (#1646)
This is an automated email from the ASF dual-hosted git repository.
dsmiley pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/main by this push:
new f3b0386f724 SOLR-14630: SolrJ queries: form the URL to the replica (#1646)
f3b0386f724 is described below
commit f3b0386f724ff69f4dcee2c6273aaf4d078d9ea3
Author: Pierre Salagnac <82...@users.noreply.github.com>
AuthorDate: Tue May 30 22:50:38 2023 +0200
SOLR-14630: SolrJ queries: form the URL to the replica (#1646)
CloudSolrClient: When querying with the `_route_` or `shards.preference` parameter and if there are multiple replicas for a collection on the receiving node, there was sometimes an extra HTTP hop or the non-intended replica may receive the request. SolrJ will now form the URL directly to the intended replica of the collection (and not the collection itself).
Co-authored-by: Pierre Salagnac <ps...@salesforce.com>
---
solr/CHANGES.txt | 2 +
.../solr/util/tracing/TestDistributedTracing.java | 4 +-
.../solr/client/solrj/impl/CloudSolrClient.java | 10 +-
.../solrj/impl/CloudSolrClientRoutingTest.java | 133 +++++++++++++++++++++
4 files changed, 145 insertions(+), 4 deletions(-)
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 5be2e73e33d..97ac08b6b91 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -162,6 +162,8 @@ Optimizations
* SOLR-16764: Clarify that ExportTool exports documents in JSON with Lines format, not standard JSON. Add explicit -compress option for gzipping output.
Add ability to specific a directory for output along with a specific file when using -out. (Eric Pugh)
+* SOLR-14630: When querying with the `_route_` or `shards.preference` parameter and if there are multiple replicas for a collection on the receiving node, there was sometimes an extra HTTP hop or the non-intended replica may receive the request. SolrJ will now form the URL directly to the intended replica of the collection (and not the collection itself). (Pierre Salagnac, Ivan Djurasevic)
+
Bug Fixes
---------------------
diff --git a/solr/core/src/test/org/apache/solr/util/tracing/TestDistributedTracing.java b/solr/core/src/test/org/apache/solr/util/tracing/TestDistributedTracing.java
index 3c51208c53f..cbfd4a64df9 100644
--- a/solr/core/src/test/org/apache/solr/util/tracing/TestDistributedTracing.java
+++ b/solr/core/src/test/org/apache/solr/util/tracing/TestDistributedTracing.java
@@ -110,8 +110,8 @@ public class TestDistributedTracing extends SolrCloudTestCase {
fail("All spans must belong to single span, but:" + finishedSpans);
}
}
- assertEquals("get:/{collection}/select", finishedSpans.get(0).operationName());
- assertDbInstanceColl(finishedSpans.get(0));
+ assertEquals("get:/{core}/select", finishedSpans.get(0).operationName());
+ assertDbInstanceCore(finishedSpans.get(0));
}
@Test
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/CloudSolrClient.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/CloudSolrClient.java
index 61ba4fb5aa7..7bed0138043 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/CloudSolrClient.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/CloudSolrClient.java
@@ -1117,8 +1117,14 @@ public abstract class CloudSolrClient extends SolrClient {
sortedReplicas.forEach(
replica -> {
if (seenNodes.add(replica.getNodeName())) {
- theUrlList.add(
- ZkCoreNodeProps.getCoreUrl(replica.getBaseUrl(), joinedInputCollections));
+ if (inputCollections.size() == 1 && collectionNames.size() == 1) {
+ // If we have a single collection name (and not a alias to multiple collection),
+ // send the query directly to a replica of this collection.
+ theUrlList.add(replica.getCoreUrl());
+ } else {
+ theUrlList.add(
+ ZkCoreNodeProps.getCoreUrl(replica.getBaseUrl(), joinedInputCollections));
+ }
}
});
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudSolrClientRoutingTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudSolrClientRoutingTest.java
new file mode 100644
index 00000000000..6b805a53d83
--- /dev/null
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudSolrClientRoutingTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.client.solrj.impl;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.common.SolrInputDocument;
+import org.apache.solr.common.params.CommonParams;
+import org.apache.solr.common.params.ShardParams;
+import org.apache.solr.common.params.UpdateParams;
+import org.apache.solr.common.util.NamedList;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public final class CloudSolrClientRoutingTest extends SolrCloudTestCase {
+
+ /**
+ * Create a cluster with 2 shards (each with a single replica) on a single node (localhost). Set
+ * "id" as the router field.
+ */
+ @BeforeClass
+ public static void setupCluster() throws Exception {
+ // configset("cloud-minimal");
+ configureCluster(1)
+ .addConfig("conf", getFile("solrj/solr/configsets/streaming/conf").toPath())
+ .configure();
+
+ CollectionAdminRequest.Create create =
+ CollectionAdminRequest.createCollection("test-collection", 2, 1);
+ create.setRouterField("id");
+ create.setReplicationFactor(1);
+ create.process(cluster.getSolrClient());
+
+ cluster.waitForActiveCollection("test-collection", 2, 2);
+ }
+
+ @Test
+ public void routeParamHandling() throws IOException, SolrServerException {
+
+ String collection = "test-collection";
+
+ SolrClient client = cluster.getSolrClient();
+
+ // Index a bunch of records with different ids
+ int numDocuments = random().nextInt(500) + 500;
+ List<SolrInputDocument> documents = new ArrayList<>(numDocuments);
+ for (int i = 0; i < 1_000; i++) {
+ String docId = Integer.toString(random().nextInt(Integer.MAX_VALUE));
+ SolrInputDocument document = new SolrInputDocument("id", docId);
+ documents.add(document);
+ }
+ client.add(collection, documents);
+ client.commit(collection, true, true);
+
+ int numForwardedWithRoute = 0;
+ int numForwardedWithoutRoute = 0;
+
+ for (int i = 0; i < 100; i++) {
+ String docId = documents.get(random().nextInt(numDocuments)).getFieldValue("id").toString();
+
+ SolrQuery queryWithoutRoute =
+ new SolrQuery(
+ UpdateParams.COLLECTION,
+ collection,
+ "q",
+ "id:" + docId,
+ CommonParams.DEBUG_QUERY,
+ "on");
+
+ // Query with _route_=<docId>.
+ SolrQuery queryWithRoute = queryWithoutRoute.getCopy().setParam(ShardParams._ROUTE_, docId);
+
+ // TRACK is present in the response only when the request is processed as a distributed
+ // request (i.e. Solr uses ShardHandler to forward the request to one or more replicas
+ // internally), so ideally it should never appear in the response since we're specifying
+ // the _route_ param, telling Solr exactly which replica the doc is in.
+ // However, the TRACK value appears randomly in the response because routing is done only to
+ // the node level, and once the request reaches the node, the replica is chosen randomly
+ // at the mercy of org.apache.solr.servlet.HttpSolrCall.randomlyGetSolrCore, and if this
+ // randomly selected replica is not the one the doc id lives in, the request is forwarded
+ // from there.
+ // The decision whether or not the request is forwarded is made at
+ // org.apache.solr.handler.component.HttpShardHandler.canShortCircuit()
+
+ boolean forwardedWithoutRoute =
+ ((NamedList<?>)
+ client
+ .query(collection, queryWithoutRoute)
+ .getResponse()
+ .get(CommonParams.DEBUG))
+ .get(CommonParams.TRACK)
+ != null;
+ if (forwardedWithoutRoute) {
+ numForwardedWithoutRoute++;
+ }
+
+ boolean forwardedWithRoute =
+ ((NamedList<?>)
+ client
+ .query(collection, queryWithRoute)
+ .getResponse()
+ .get(CommonParams.DEBUG))
+ .get(CommonParams.TRACK)
+ != null;
+ if (forwardedWithRoute) {
+ numForwardedWithRoute++;
+ }
+ }
+
+ assertEquals(0, numForwardedWithRoute);
+ assertEquals(100, numForwardedWithoutRoute);
+ }
+}