Skip to content

Commit 7d151fb

Browse files
committed
SOLR-18130: add universal connection string support for CloudSolrClient.Builder
1 parent db8a6d3 commit 7d151fb

6 files changed

Lines changed: 304 additions & 21 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
title: Added universal connection string support into CloudSolrClient.Builder
2+
type: added
3+
authors:
4+
- name: Vladimir Vyatkin
5+
links:
6+
- name: SOLR-18130
7+
url: https://issues.apache.org/jira/browse/SOLR-18130

solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/SolrClientCache.java

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import java.io.Closeable;
2020
import java.util.HashMap;
21-
import java.util.List;
2221
import java.util.Map;
2322
import java.util.Objects;
2423
import java.util.Optional;
@@ -33,6 +32,7 @@
3332
import org.apache.solr.client.solrj.jetty.HttpJettySolrClient;
3433
import org.apache.solr.common.AlreadyClosedException;
3534
import org.apache.solr.common.util.IOUtils;
35+
import org.apache.solr.common.util.StrUtils;
3636
import org.apache.solr.common.util.URLUtil;
3737

3838
/** The SolrClientCache caches SolrClients, so they can be reused by different TupleStreams. */
@@ -77,29 +77,37 @@ public void setDefaultZKHost(String zkHost) {
7777
}
7878
}
7979

80-
public synchronized CloudSolrClient getCloudSolrClient(String zkHost) {
80+
public synchronized CloudSolrClient getCloudSolrClient(String connectionString) {
8181
ensureOpen();
82-
Objects.requireNonNull(zkHost, "ZooKeeper host cannot be null!");
83-
if (solrClients.containsKey(zkHost)) {
84-
return (CloudSolrClient) solrClients.get(zkHost);
82+
Objects.requireNonNull(connectionString, "Connection string cannot be null!");
83+
if (solrClients.containsKey(connectionString)) {
84+
return (CloudSolrClient) solrClients.get(connectionString);
8585
}
8686
// Can only use ZK ACLs if there is a default ZK Host, and the given ZK host contains that
8787
// default.
8888
// Basically the ZK ACLs are assumed to be only used for the default ZK host,
8989
// thus we should only provide the ACLs to that Zookeeper instance.
90-
String zkHostNoChroot = zkHost.split("/")[0];
91-
boolean canUseACLs =
92-
Optional.ofNullable(defaultZkHost.get()).map(zkHostNoChroot::equals).orElse(false);
90+
boolean canUseACLs = false;
91+
CloudSolrClient.CloudSolrClientConnection cloudClientConnection =
92+
CloudSolrClient.CloudSolrClientConnection.parse(connectionString);
93+
if (cloudClientConnection.isZk()) {
94+
String chroot = cloudClientConnection.zkChroot();
95+
String zkHostNoChroot =
96+
StrUtils.isNotBlank(chroot) && connectionString.endsWith(chroot)
97+
? connectionString.substring(0, connectionString.length() - chroot.length())
98+
: connectionString;
99+
canUseACLs =
100+
Optional.ofNullable(defaultZkHost.get()).map(zkHostNoChroot::equals).orElse(false);
101+
}
93102

94-
final var client = newCloudSolrClient(zkHost, httpSolrClient, canUseACLs);
95-
solrClients.put(zkHost, client);
103+
final var client = newCloudSolrClient(connectionString, httpSolrClient, canUseACLs);
104+
solrClients.put(connectionString, client);
96105
return client;
97106
}
98107

99108
protected CloudSolrClient newCloudSolrClient(
100-
String zkHost, HttpSolrClientBase httpSolrClient, boolean canUseACLs) {
101-
final List<String> hosts = List.of(zkHost);
102-
var builder = new CloudSolrClient.Builder(hosts, Optional.empty());
109+
String connectionString, HttpSolrClientBase httpSolrClient, boolean canUseACLs) {
110+
var builder = new CloudSolrClient.Builder(connectionString);
103111
builder.canUseZkACLs(canUseACLs);
104112
// using internal builder to ensure the internal client gets closed
105113
builder = builder.withHttpClientBuilder(newHttpSolrClientBuilder(null, httpSolrClient));

solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/SolrClientCacheTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@
1717
package org.apache.solr.client.solrj.io;
1818

1919
import java.util.Map;
20+
import org.apache.solr.client.solrj.impl.CloudSolrClient;
2021
import org.apache.solr.cloud.SolrCloudTestCase;
2122
import org.apache.solr.common.SolrException;
23+
import org.apache.solr.common.cloud.ClusterState;
2224
import org.apache.solr.common.cloud.DigestZkACLProvider;
2325
import org.apache.solr.common.cloud.DigestZkCredentialsProvider;
2426
import org.apache.solr.common.cloud.SolrZkClient;
2527
import org.apache.solr.common.cloud.VMParamsZkCredentialsInjector;
2628
import org.junit.AfterClass;
29+
import org.junit.Assert;
2730
import org.junit.BeforeClass;
2831
import org.junit.Test;
2932

@@ -66,6 +69,27 @@ public void testZkACLsNotUsedWithDifferentZkHost() {
6669
}
6770
}
6871

72+
@Test
73+
public void testGetClientWithHttp() {
74+
String solrUrl = cluster.getJettySolrRunner(0).getBaseUrl().toString();
75+
try (SolrClientCache cache = new SolrClientCache()) {
76+
CloudSolrClient cloudSolrClient = cache.getCloudSolrClient(solrUrl);
77+
ClusterState clusterState = cloudSolrClient.getClusterStateProvider().getClusterState();
78+
Assert.assertEquals(1, clusterState.getLiveNodes().size());
79+
}
80+
}
81+
82+
@Test
83+
public void testGetClientWithZookeeper() {
84+
String zkConnectionString = zkClient().getZkServerAddress();
85+
try (SolrClientCache cache = new SolrClientCache()) {
86+
cache.setDefaultZKHost(zkClient().getZkServerAddress());
87+
CloudSolrClient cloudSolrClient = cache.getCloudSolrClient(zkConnectionString);
88+
ClusterState clusterState = cloudSolrClient.getClusterStateProvider().getClusterState();
89+
Assert.assertEquals(1, clusterState.getLiveNodes().size());
90+
}
91+
}
92+
6993
@Test
7094
public void testZkACLsUsedWithDifferentChroot() {
7195
try (SolrClientCache cache = new SolrClientCache()) {

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

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,30 @@ public Builder(ClusterStateProvider stateProvider) {
241241
this.stateProvider = stateProvider;
242242
}
243243

244+
/**
245+
* Creates a client builder based on a connection string of 2 possible formats:
246+
*
247+
* <ul>
248+
* <li>ZooKeeper connection string (optionally with chroot), e.g. {@code
249+
* zk1:2181,zk2:2181,zk3:2181/solr}
250+
* <li>Comma-separated list of Solr node base URLs (HTTP or HTTPS), e.g. {@code
251+
* http://solr1:8983/solr,http://solr2:8983/solr}
252+
* </ul>
253+
*
254+
* @param connectionString a string specifying either ZooKeeper connection string or HTTP(S)
255+
* Solr URLs
256+
* @throws IllegalArgumentException if string is null, empty, or malformed
257+
*/
258+
public Builder(String connectionString) {
259+
CloudSolrClientConnection connStr = CloudSolrClientConnection.parse(connectionString);
260+
if (connStr.isZk()) {
261+
this.zkHosts = connStr.quorumItems();
262+
this.zkChroot = connStr.zkChroot();
263+
} else {
264+
this.solrUrls = connStr.quorumItems();
265+
}
266+
}
267+
244268
/** Whether to use the default ZK ACLs when building a ZK Client. */
245269
public Builder canUseZkACLs(boolean canUseZkACLs) {
246270
this.canUseZkACLs = canUseZkACLs;
@@ -1842,4 +1866,52 @@ private static boolean hasInfoToFindLeaders(UpdateRequest updateRequest, String
18421866

18431867
return true;
18441868
}
1869+
1870+
/** Universal connection string parser logic. */
1871+
public record CloudSolrClientConnection(boolean isZk, List<String> quorumItems, String zkChroot) {
1872+
1873+
public CloudSolrClientConnection {
1874+
if (quorumItems == null || quorumItems.isEmpty()) {
1875+
throw new IllegalArgumentException("No valid hosts/urls found");
1876+
}
1877+
}
1878+
1879+
public static CloudSolrClientConnection parse(String connectionString) {
1880+
if (connectionString == null || connectionString.trim().isEmpty()) {
1881+
throw new IllegalArgumentException("Connection string must not be null or empty");
1882+
}
1883+
connectionString = connectionString.trim();
1884+
if (connectionString.contains("://")) {
1885+
return parseHttpQuorum(connectionString);
1886+
}
1887+
return parseZkQuorum(connectionString);
1888+
}
1889+
1890+
private static CloudSolrClientConnection parseZkQuorum(String connectionString) {
1891+
String zkChroot = null;
1892+
String zkHosts = connectionString;
1893+
int slashIndex = connectionString.indexOf('/');
1894+
if (slashIndex != -1) {
1895+
zkHosts = connectionString.substring(0, slashIndex);
1896+
zkChroot = connectionString.substring(slashIndex);
1897+
}
1898+
List<String> quorumItems = StrUtils.split(zkHosts, ',');
1899+
for (String host : quorumItems) {
1900+
if (host == null || host.isBlank()) {
1901+
throw new IllegalArgumentException("Empty host in Zookeeper connection string");
1902+
}
1903+
}
1904+
return new CloudSolrClientConnection(true, quorumItems, zkChroot);
1905+
}
1906+
1907+
private static CloudSolrClientConnection parseHttpQuorum(String connectionString) {
1908+
List<String> quorumItems = StrUtils.split(connectionString, ',');
1909+
for (String url : quorumItems) {
1910+
if (url == null || url.isBlank()) {
1911+
throw new IllegalArgumentException("Empty URL in HTTP(S) connection string");
1912+
}
1913+
}
1914+
return new CloudSolrClientConnection(false, quorumItems, null);
1915+
}
1916+
}
18451917
}

solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudHttp2SolrClientTest.java

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ public class CloudHttp2SolrClientTest extends SolrCloudTestCase {
105105
private static CloudHttp2SolrClient httpJettyBasedCloudSolrClient = null;
106106
private static CloudHttp2SolrClient httpJdkBasedCloudSolrClient = null;
107107
private static CloudHttp2SolrClient zkBasedCloudSolrClient = null;
108+
private static CloudHttp2SolrClient connectionStringZkBasedCloudSolrClient = null;
109+
private static CloudHttp2SolrClient connectionStringHttpBasedCloudSolrClient = null;
108110

109111
@BeforeClass
110112
public static void setupCluster() throws Exception {
@@ -161,30 +163,51 @@ public static void setupCluster() throws Exception {
161163
assertTrue(zkBasedCloudSolrClient.getHttpClient() instanceof HttpJettySolrClient);
162164
assertTrue(
163165
zkBasedCloudSolrClient.getClusterStateProvider() instanceof ZkClientClusterStateProvider);
166+
167+
String zkConnString = cluster.getZkServer().getZkAddress();
168+
connectionStringZkBasedCloudSolrClient = new CloudSolrClient.Builder(zkConnString).build();
169+
assertTrue(
170+
connectionStringZkBasedCloudSolrClient.getHttpClient() instanceof HttpJettySolrClient);
171+
assertTrue(
172+
connectionStringZkBasedCloudSolrClient.getClusterStateProvider()
173+
instanceof ZkClientClusterStateProvider);
174+
175+
String httpConnString = String.join(",", solrUrls);
176+
connectionStringHttpBasedCloudSolrClient = new CloudSolrClient.Builder(httpConnString).build();
177+
assertTrue(
178+
connectionStringHttpBasedCloudSolrClient.getHttpClient() instanceof HttpJettySolrClient);
179+
assertTrue(
180+
connectionStringHttpBasedCloudSolrClient.getClusterStateProvider()
181+
instanceof HttpClusterStateProvider<?>);
164182
}
165183

166184
@AfterClass
167185
public static void tearDownAfterClass() throws Exception {
168186
IOUtils.closeQuietly(httpJettyBasedCloudSolrClient);
169187
IOUtils.closeQuietly(httpJdkBasedCloudSolrClient);
170188
IOUtils.closeQuietly(zkBasedCloudSolrClient);
189+
IOUtils.closeQuietly(connectionStringZkBasedCloudSolrClient);
190+
IOUtils.closeQuietly(connectionStringHttpBasedCloudSolrClient);
171191

172192
shutdownCluster();
173193
httpJettyBasedCloudSolrClient = null;
174194
httpJdkBasedCloudSolrClient = null;
175195
zkBasedCloudSolrClient = null;
196+
connectionStringZkBasedCloudSolrClient = null;
197+
connectionStringHttpBasedCloudSolrClient = null;
176198
}
177199

178200
/** Randomly return the cluster's ZK based CSC, or HttpClusterProvider based CSC. */
179201
private CloudSolrClient getRandomClient() {
180-
int randInt = random().nextInt(3);
181-
if (randInt == 0) {
182-
return zkBasedCloudSolrClient;
183-
}
184-
if (randInt == 1) {
185-
return httpJettyBasedCloudSolrClient;
186-
}
187-
return httpJdkBasedCloudSolrClient;
202+
CloudSolrClient[] clients = {
203+
zkBasedCloudSolrClient,
204+
httpJettyBasedCloudSolrClient,
205+
httpJdkBasedCloudSolrClient,
206+
connectionStringZkBasedCloudSolrClient,
207+
connectionStringHttpBasedCloudSolrClient
208+
};
209+
210+
return clients[random().nextInt(clients.length)];
188211
}
189212

190213
@Test

0 commit comments

Comments
 (0)