Skip to content

Commit c5709bd

Browse files
authored
SOLR-17066: Switch "LB" clients away from core URLs (#2283)
Providing a core URL String as a SolrClient's "base URL" prevents it from communicating with other cores or making core-agnostic API requests (e.g. node healthcheck, list cores, etc.) This commit migrates usage of both "LB" clients away from using raw core-URL Strings, in favor of using a new structured 'Endpoint' class, which allows access to both the "root URL" and "collection" separately. This commit also updates various ref-guides and Javadoc snippets to reflect the fact that clients now only accept "root URLs" (with a small exception for LB clients which may use the Endpoint class, as mentioned above).
1 parent b187595 commit c5709bd

28 files changed

+684
-439
lines changed

solr/CHANGES.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ Deprecation Removals
5252

5353
* SOLR-17159: Remove deprecated bin/post and bin/postlogs scripts in favour of bin/solr equivalents. (Eric Pugh)
5454

55+
* SOLR-17066: `SolrClient` implementations that rely on "base URL" strings now only accept "root" URL paths (i.e. URLs that
56+
end in "/solr"). Users may still specify a default collection through use of the `withDefaultCollection` method available
57+
on most SolrClient builders. (Jason Gerlowski, David Smiley, Eric Pugh)
58+
59+
5560
Dependency Upgrades
5661
---------------------
5762
(No changes)

solr/core/src/java/org/apache/solr/handler/component/HttpShardHandlerFactory.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.util.concurrent.ExecutorService;
2929
import java.util.concurrent.SynchronousQueue;
3030
import java.util.concurrent.TimeUnit;
31+
import java.util.stream.Collectors;
3132
import org.apache.solr.client.solrj.SolrClient;
3233
import org.apache.solr.client.solrj.impl.Http2SolrClient;
3334
import org.apache.solr.client.solrj.impl.HttpClientUtil;
@@ -347,7 +348,10 @@ protected LBSolrClient.Req newLBHttpSolrClientReq(final QueryRequest req, List<S
347348
if (numServersToTry < this.permittedLoadBalancerRequestsMinimumAbsolute) {
348349
numServersToTry = this.permittedLoadBalancerRequestsMinimumAbsolute;
349350
}
350-
return new LBSolrClient.Req(req, urls, numServersToTry);
351+
352+
final var endpoints =
353+
urls.stream().map(url -> LBSolrClient.Endpoint.from(url)).collect(Collectors.toList());
354+
return new LBSolrClient.Req(req, endpoints, numServersToTry);
351355
}
352356

353357
/**

solr/core/src/test/org/apache/solr/cloud/SSLMigrationTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ private void setUrlScheme(String value) throws Exception {
127127
.map(r -> r.getStr(ZkStateReader.BASE_URL_PROP))
128128
.toArray(String[]::new);
129129
// Create new SolrServer to configure new HttpClient w/ SSL config
130-
try (SolrClient client = new LBHttpSolrClient.Builder().withBaseSolrUrls(urls).build()) {
130+
try (SolrClient client = new LBHttpSolrClient.Builder().withBaseEndpoints(urls).build()) {
131131
client.request(request);
132132
}
133133
}

solr/core/src/test/org/apache/solr/handler/component/DistributedDebugComponentTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,7 @@ public void testCompareWithNonDistributedRequest() throws SolrServerException, I
403403
}
404404

405405
public void testTolerantSearch() throws SolrServerException, IOException {
406-
String badShard = DEAD_HOST_1;
406+
String badShard = DEAD_HOST_1 + "/solr/collection1";
407407
SolrQuery query = new SolrQuery();
408408
query.setQuery("*:*");
409409
query.set("debug", "true");

solr/core/src/test/org/apache/solr/handler/component/TestHttpShardHandlerFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public void testLoadBalancerRequestsMinMax() {
9393
final QueryRequest queryRequest = null;
9494
final List<String> urls = new ArrayList<>();
9595
for (int ii = 0; ii < 10; ++ii) {
96-
urls.add(null);
96+
urls.add("http://localhost" + ii + ":8983/solr");
9797
}
9898

9999
// create LBHttpSolrClient request

solr/core/src/test/org/apache/solr/update/DeleteByIdWithRouterFieldTest.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,9 @@ public void testGlassBoxUpdateRequestRoutesToShards() {
303303
final Map<String, List<String>> urlMap =
304304
docCol.getActiveSlices().stream()
305305
.collect(
306-
Collectors.toMap(s -> s.getName(), s -> Collections.singletonList(s.getName())));
306+
Collectors.toMap(
307+
s -> s.getName(),
308+
s -> Collections.singletonList(fakeSolrUrlForShard(s.getName()))));
307309

308310
// simplified rote info we'll build up with the shards for each delete (after sanity checking
309311
// they have routing info at all)...
@@ -314,7 +316,7 @@ public void testGlassBoxUpdateRequestRoutesToShards() {
314316
.getRoutesToCollection(docCol.getRouter(), docCol, urlMap, params(), ROUTE_FIELD);
315317
for (LBSolrClient.Req lbreq : rawDelRoutes.values()) {
316318
assertTrue(lbreq.getRequest() instanceof UpdateRequest);
317-
final String shard = lbreq.getServers().get(0);
319+
final LBSolrClient.Endpoint shard = lbreq.getEndpoints().get(0);
318320
final UpdateRequest req = (UpdateRequest) lbreq.getRequest();
319321
for (Map.Entry<String, Map<String, Object>> entry : req.getDeleteByIdMap().entrySet()) {
320322
final String id = entry.getKey();
@@ -327,7 +329,7 @@ public void testGlassBoxUpdateRequestRoutesToShards() {
327329
RVAL_PRE + id.substring(id.length() - 1),
328330
route.toString());
329331

330-
actualDelRoutes.put(id, shard);
332+
actualDelRoutes.put(id, shard.toString());
331333
}
332334
}
333335

@@ -342,11 +344,18 @@ public void testGlassBoxUpdateRequestRoutesToShards() {
342344
.getRouter()
343345
.getTargetSlice(id, doc, doc.getFieldValue(ROUTE_FIELD).toString(), params(), docCol);
344346
assertNotNull(id + " add route is null?", expectedShard);
345-
assertEquals("Wrong shard for delete of id: " + id, expectedShard.getName(), actualShard);
347+
assertEquals(
348+
"Wrong shard for delete of id: " + id,
349+
fakeSolrUrlForShard(expectedShard.getName()),
350+
actualShard.toString());
346351
}
347352

348353
// sanity check no one broke our test and made it a waste of time
349354
assertEquals(100, add100Docs().getDocuments().size());
350355
assertEquals(100, actualDelRoutes.entrySet().size());
351356
}
357+
358+
private static String fakeSolrUrlForShard(String shardName) {
359+
return "http://localhost:8983/solr/" + shardName;
360+
}
352361
}

solr/solr-ref-guide/modules/deployment-guide/pages/solrj.adoc

Lines changed: 30 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -117,51 +117,48 @@ The most common/important of these are discussed below.
117117
For comprehensive information on how to tweak your `SolrClient`, see the Javadocs for the involved client, and its corresponding builder object.
118118

119119
==== Base URLs
120-
Most `SolrClient` implementations (except for `CloudSolrClient` and `Http2SolrClient`) require users to specify one or more Solr base URLs, which the client then uses to send HTTP requests to Solr.
121-
The path users include on the base URL they provide has an effect on the behavior of the created client from that point on.
122-
123-
. A URL with a path pointing to a specific core or collection (e.g., `\http://hostname:8983/solr/core1`).
124-
When a core or collection is specified in the base URL, subsequent requests made with that client are not required to re-specify the affected collection.
125-
However, the client is limited to sending requests to that core/collection, and can not send requests to any others.
126-
. A URL pointing to the root Solr path (e.g., `\http://hostname:8983/solr`).
127-
When no core or collection is specified in the base URL, requests can be made to any core/collection, but the affected core/collection must be specified on all requests.
128-
129-
Generally speaking, if your `SolrClient` will only be used on a single core/collection, including that entity in the path is the most convenient.
130-
Where more flexibility is required, the collection/core should be excluded.
131-
132-
==== Base URLs of Http2SolrClient
133-
The `Http2SolrClient` manages connections to different nodes efficiently.
134-
`Http2SolrClient` does not require a `baseUrl`.
135-
In case a `baseUrl` is not provided, then `SolrRequest.basePath` must be set, so
136-
`Http2SolrClient` knows which nodes to send requests to.
137-
If not an `IllegalArgumentException` will be thrown.
138-
139-
==== Base URLs of CloudSolrClient
140-
141-
It is also possible to specify base URLs for `CloudSolrClient`, but URLs are expected to point to the root Solr path (e.g., `\http://hostname:8983/solr`).
142-
They should not include any collections, cores, or other path components.
143-
120+
Many `SolrClient` implementations require users to specify one or more Solr URLs, which the client then uses to send HTTP requests to Solr.
121+
Unless otherwise specified, SolrJ expects these URLs to point to the root Solr path (i.e. "/solr").
122+
123+
A few notable exceptions to this are described below:
124+
125+
- *Http2SolrClient* - Users of `Http2SolrClient` may choose to skip providing a root URL to their client, in favor of specifing the URL on a request-by-request basis using `SolrRequest.setBasePath`.
126+
`Http2SolrClient` will throw an `IllegalArgumentException` if neither the client nor the request specify a URL.
127+
- *LBHttpSolrClient* and *LBHttp2SolrClient* - Solr's "load balancing" clients are frequently used to round-robin requests across a set of replicas or cores.
128+
URLs are still expected to point to the Solr root (i.e. "/solr"), but to support this use-case the URLs are often supplemented by an additional parameter to specify the targeted core.
129+
Alternatively, some "load balancing" methods make use of an `Endpoint` abstraction to provide this URL and core information in a more structured way.
130+
- *CloudSolrClient* - Like many clients, CloudSolrClient accepts a series of URLs pointing to the Solr root path (i.e. "/solr").
131+
+
144132
[source,java,indent=0]
145133
----
146134
include::example$UsingSolrJRefGuideExamplesTest.java[tag=solrj-cloudsolrclient-baseurl]
147135
----
148-
149-
In case a `baseUrl` is not provided, then a list of ZooKeeper hosts (with ports) and ZooKeeper root must be provided.
150-
If no ZooKeeper root is used then `java.util.Optional.empty()` has to be provided as part of the method.
151-
136+
+
137+
However, unlike other clients, these URLs aren't used to send user-provided requests, but instead serve to fetch information about the layout and health of the Solr cluster.
138+
If Solr URLs are not known, users may instead specify a list of ZooKeeper hosts (with ports) and ZooKeeper root path, for the `CloudSolrClient` to fetch this information from ZooKeeper directly.
139+
+
152140
[source,java,indent=0]
153141
----
154-
include::example$UsingSolrJRefGuideExamplesTest.java[tag=solrj-cloudsolrclient-zookeepernoroot]
142+
include::example$UsingSolrJRefGuideExamplesTest.java[tag=solrj-cloudsolrclient-zookeeperroot]
155143
----
156-
144+
+
145+
If no ZooKeeper root path is used then `java.util.Optional.empty()` should be provided.
146+
+
157147
[source,java,indent=0]
158148
----
159-
include::example$UsingSolrJRefGuideExamplesTest.java[tag=solrj-cloudsolrclient-zookeeperroot]
149+
include::example$UsingSolrJRefGuideExamplesTest.java[tag=solrj-cloudsolrclient-zookeepernoroot]
160150
----
151+
+
152+
`CloudSolrClient` users who wish to provide ZooKeeper connection information, must depend on the `solr-solrj-zookeeper` artifact to have all of the necessary classes available.
153+
The ZooKeeper based connection is the most reliable and performant means for CloudSolrClient to work. On the other hand, it means exposing ZooKeeper more broadly than to Solr nodes, which is a security risk. It also adds more JAR dependencies.
161154

162-
Additionally, you will need to depend on the `solr-solrj-zookeeper` artifact or else you will get a `ClassNotFoundException`.
155+
==== Default Collections
163156

164-
The ZooKeeper based connection is the most reliable and performant means for CloudSolrClient to work. On the other hand, it means exposing ZooKeeper more broadly than to Solr nodes, which is a security risk. It also adds more JAR dependencies.
157+
Most `SolrClient` methods allow users to specify the core or collection they wish to query, etc. as a `String` parameter.
158+
However continually specifying this parameter can become tedious, especially for users who always work with the same collection.
159+
160+
Users can avoid this pattern by specifying a "default" collection when creating their client, using the `withDefaultCollection(String)` method available on the relevant `SolrClient` Builder object.
161+
If specified on a Builder, the created `SolrClient` will use this default for making requests whenever a collection or core is needed (and no overriding value is specified).
165162

166163
==== Timeouts
167164
All `SolrClient` implementations allow users to specify the connection and read timeouts for communicating with Solr.

solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ Before starting an upgrade to this version of Solr, please take the time to revi
2828

2929
=== SolrJ
3030

31-
Starting in 10, the Maven POM for SolrJ does not refer to SolrJ modules like ZooKeeper. If you require such functionality, you need to add additional dependencies.
31+
* Starting in 10, the Maven POM for SolrJ does not refer to SolrJ modules like ZooKeeper. If you require such functionality, you need to add additional dependencies.
32+
33+
* `SolrClient` implementations that rely on "base URL" strings now only accept "root" URL paths (i.e. URLs that end in "/solr").
34+
Users who previously relied on collection-specific URLs to avoid including the collection name with each request can instead achieve this by specifying a "default collection" using the `withDefaultCollection` method available on most `SolrClient` Builders.
3235

3336
=== Deprecation removals
3437

@@ -47,4 +50,4 @@ has been removed. Please use `-Dsolr.hiddenSysProps` or the envVar `SOLR_HIDDEN_
4750

4851
* The node configuration file `/solr.xml` can no longer be loaded from Zookeeper. Solr startup will fail if it is present.
4952

50-
* The legacy Circuit Breaker named `CircuitBreakerManager`, is removed. Please use individual Circuit Breaker plugins instead.
53+
* The legacy Circuit Breaker named `CircuitBreakerManager`, is removed. Please use individual Circuit Breaker plugins instead.

solr/solrj/src/java/org/apache/solr/client/solrj/impl/CloudSolrClient.java

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -492,14 +492,18 @@ private NamedList<Object> directUpdate(AbstractUpdateRequest request, String col
492492
nonRoutableRequest.setParams(nonRoutableParams);
493493
nonRoutableRequest.setBasicAuthCredentials(
494494
request.getBasicAuthUser(), request.getBasicAuthPassword());
495-
List<String> urlList = new ArrayList<>(routes.keySet());
496-
Collections.shuffle(urlList, rand);
497-
LBSolrClient.Req req = new LBSolrClient.Req(nonRoutableRequest, urlList);
495+
final var endpoints =
496+
routes.keySet().stream()
497+
.map(url -> LBSolrClient.Endpoint.from(url))
498+
.collect(Collectors.toList());
499+
Collections.shuffle(endpoints, rand);
500+
LBSolrClient.Req req = new LBSolrClient.Req(nonRoutableRequest, endpoints);
498501
try {
499502
LBSolrClient.Rsp rsp = getLbClient().request(req);
500-
shardResponses.add(urlList.get(0), rsp.getResponse());
503+
shardResponses.add(endpoints.get(0).toString(), rsp.getResponse());
501504
} catch (Exception e) {
502-
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, urlList.get(0), e);
505+
throw new SolrException(
506+
SolrException.ErrorCode.SERVER_ERROR, endpoints.get(0).toString(), e);
503507
}
504508
}
505509

@@ -1018,18 +1022,21 @@ protected NamedList<Object> sendRequest(SolrRequest<?> request, List<String> inp
10181022
final String urlScheme = provider.getClusterProperty(ClusterState.URL_SCHEME, "http");
10191023
final Set<String> liveNodes = provider.getLiveNodes();
10201024

1021-
final List<String> theUrlList = new ArrayList<>(); // we populate this as follows...
1025+
final List<LBSolrClient.Endpoint> requestEndpoints =
1026+
new ArrayList<>(); // we populate this as follows...
10221027

10231028
if (request instanceof V2Request) {
10241029
if (!liveNodes.isEmpty()) {
10251030
List<String> liveNodesList = new ArrayList<>(liveNodes);
10261031
Collections.shuffle(liveNodesList, rand);
1027-
theUrlList.add(Utils.getBaseUrlForNodeName(liveNodesList.get(0), urlScheme));
1032+
final var chosenNodeUrl = Utils.getBaseUrlForNodeName(liveNodesList.get(0), urlScheme);
1033+
requestEndpoints.add(new LBSolrClient.Endpoint(chosenNodeUrl));
10281034
}
10291035

10301036
} else if (ADMIN_PATHS.contains(request.getPath())) {
10311037
for (String liveNode : liveNodes) {
1032-
theUrlList.add(Utils.getBaseUrlForNodeName(liveNode, urlScheme));
1038+
final var nodeBaseUrl = Utils.getBaseUrlForNodeName(liveNode, urlScheme);
1039+
requestEndpoints.add(new LBSolrClient.Endpoint(nodeBaseUrl));
10331040
}
10341041

10351042
} else { // Typical...
@@ -1044,13 +1051,13 @@ protected NamedList<Object> sendRequest(SolrRequest<?> request, List<String> inp
10441051
List<String> preferredNodes = request.getPreferredNodes();
10451052
if (preferredNodes != null && !preferredNodes.isEmpty()) {
10461053
String joinedInputCollections = StrUtils.join(inputCollections, ',');
1047-
List<String> urlList = new ArrayList<>(preferredNodes.size());
1048-
for (String nodeName : preferredNodes) {
1049-
urlList.add(
1050-
Utils.getBaseUrlForNodeName(nodeName, urlScheme) + "/" + joinedInputCollections);
1051-
}
1052-
if (!urlList.isEmpty()) {
1053-
LBSolrClient.Req req = new LBSolrClient.Req(request, urlList);
1054+
final var endpoints =
1055+
preferredNodes.stream()
1056+
.map(nodeName -> Utils.getBaseUrlForNodeName(nodeName, urlScheme))
1057+
.map(nodeUrl -> new LBSolrClient.Endpoint(nodeUrl, joinedInputCollections))
1058+
.collect(Collectors.toList());
1059+
if (!endpoints.isEmpty()) {
1060+
LBSolrClient.Req req = new LBSolrClient.Req(request, endpoints);
10541061
LBSolrClient.Rsp rsp = getLbClient().request(req);
10551062
return rsp.getResponse();
10561063
}
@@ -1110,23 +1117,24 @@ protected NamedList<Object> sendRequest(SolrRequest<?> request, List<String> inp
11101117
if (inputCollections.size() == 1 && collectionNames.size() == 1) {
11111118
// If we have a single collection name (and not a alias to multiple collection),
11121119
// send the query directly to a replica of this collection.
1113-
theUrlList.add(replica.getCoreUrl());
1120+
requestEndpoints.add(
1121+
new LBSolrClient.Endpoint(replica.getBaseUrl(), replica.getCoreName()));
11141122
} else {
1115-
theUrlList.add(
1116-
ZkCoreNodeProps.getCoreUrl(replica.getBaseUrl(), joinedInputCollections));
1123+
requestEndpoints.add(
1124+
new LBSolrClient.Endpoint(replica.getBaseUrl(), joinedInputCollections));
11171125
}
11181126
}
11191127
});
11201128

1121-
if (theUrlList.isEmpty()) {
1129+
if (requestEndpoints.isEmpty()) {
11221130
collectionStateCache.keySet().removeAll(collectionNames);
11231131
throw new SolrException(
11241132
SolrException.ErrorCode.INVALID_STATE,
11251133
"Could not find a healthy node to handle the request.");
11261134
}
11271135
}
11281136

1129-
LBSolrClient.Req req = new LBSolrClient.Req(request, theUrlList);
1137+
LBSolrClient.Req req = new LBSolrClient.Req(request, requestEndpoints);
11301138
LBSolrClient.Rsp rsp = getLbClient().request(req);
11311139
return rsp.getResponse();
11321140
}

solr/solrj/src/java/org/apache/solr/client/solrj/impl/ConcurrentUpdateHttp2SolrClient.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,10 +709,48 @@ public static class Builder {
709709
protected boolean closeHttp2Client;
710710
private long pollQueueTimeMillis;
711711

712+
/**
713+
* Initialize a Builder object, based on the provided URL and client.
714+
*
715+
* <p>The provided URL must point to the root Solr path (i.e. "/solr"), for example:
716+
*
717+
* <pre>
718+
* SolrClient client = new ConcurrentUpdateHttp2SolrClient.Builder("http://my-solr-server:8983/solr", http2Client)
719+
* .withDefaultCollection("core1")
720+
* .build();
721+
* QueryResponse resp = client.query(new SolrQuery("*:*"));
722+
* </pre>
723+
*
724+
* @param baseSolrUrl a URL pointing to the root Solr path, typically of the form
725+
* "http[s]://host:port/solr"
726+
* @param client a client for this ConcurrentUpdateHttp2SolrClient to use for all requests
727+
* internally. Callers are responsible for closing the provided client (after closing any
728+
* clients created by this builder)
729+
*/
712730
public Builder(String baseSolrUrl, Http2SolrClient client) {
713731
this(baseSolrUrl, client, false);
714732
}
715733

734+
/**
735+
* Initialize a Builder object, based on the provided arguments.
736+
*
737+
* <p>The provided URL must point to the root Solr path (i.e. "/solr"), for example:
738+
*
739+
* <pre>
740+
* SolrClient client = new ConcurrentUpdateHttp2SolrClient.Builder("http://my-solr-server:8983/solr", http2Client)
741+
* .withDefaultCollection("core1")
742+
* .build();
743+
* QueryResponse resp = client.query(new SolrQuery("*:*"));
744+
* </pre>
745+
*
746+
* @param baseSolrUrl a URL pointing to the root Solr path, typically of the form
747+
* "http[s]://host:port/solr"
748+
* @param client a client for this ConcurrentUpdateHttp2SolrClient to use for all requests
749+
* internally.
750+
* @param closeHttp2Client a boolean flag indicating whether the created
751+
* ConcurrentUpdateHttp2SolrClient should assume responsibility for closing the provided
752+
* 'client'
753+
*/
716754
public Builder(String baseSolrUrl, Http2SolrClient client, boolean closeHttp2Client) {
717755
this.baseSolrUrl = baseSolrUrl;
718756
this.client = client;

0 commit comments

Comments
 (0)