SOLR-17194: Fix bug - CloudHttp2SolrClient doesn't preserve the passed httpClient to Http2ClusterStateProvider when using the internalClientBuilder (#2412)
Co-authored-by: Lamine Idjeraoui <lidjeraoui@apple.com>
Co-authored-by: Eric Pugh <epugh@opensourceconnections.com>
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 1ce3bd0..fc4b687 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -191,9 +191,10 @@
* SOLR-16866: Reorder nested directory deletions to avoid logging spurious NoSuchFileException (Michael Gibney)
+* SOLR-17194: Fix CloudHttp2SolrClient doesn't preserve the passed httpClient to Http2ClusterStateProvider when using the internalClientBuilder (Eric Pugh, Lamine Idjeraoui, Houston Putman, Anshum Gupta)
+
Dependency Upgrades
---------------------
-
* SOLR-17157: Upgrade Lucene to 9.10.0 (hossman, Christine Poerschke)
* SOLR-17214: Update forbiddenapis to 3.7 (hossman)
@@ -202,6 +203,7 @@
Other Changes
---------------------
+
* SOLR-17126: Cut over System.getProperty to EnvUtils.getProperty (janhoy)
* SOLR-17066: GenericSolrRequest now has a `setRequiresCollection` setter that allows it to specify whether
diff --git a/solr/core/src/test/org/apache/solr/cli/TestExportTool.java b/solr/core/src/test/org/apache/solr/cli/TestExportTool.java
index 0aa9bfb..88b4e4a 100644
--- a/solr/core/src/test/org/apache/solr/cli/TestExportTool.java
+++ b/solr/core/src/test/org/apache/solr/cli/TestExportTool.java
@@ -17,6 +17,12 @@
package org.apache.solr.cli;
+import static java.util.Collections.singletonList;
+import static java.util.Collections.singletonMap;
+import static org.apache.solr.cli.SolrCLI.findTool;
+import static org.apache.solr.cli.SolrCLI.parseCmdLine;
+import static org.apache.solr.security.Sha256AuthenticationProvider.getSaltedHashedValue;
+
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
@@ -27,6 +33,7 @@
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
+import org.apache.commons.cli.CommandLine;
import org.apache.lucene.tests.util.TestUtil;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.client.solrj.SolrClient;
@@ -43,10 +50,15 @@
import org.apache.solr.common.cloud.Slice;
import org.apache.solr.common.util.FastInputStream;
import org.apache.solr.common.util.JsonRecordReader;
+import org.apache.solr.common.util.Utils;
+import org.apache.solr.security.BasicAuthPlugin;
+import org.apache.solr.security.RuleBasedAuthorizationPlugin;
+import org.junit.Test;
@SolrTestCaseJ4.SuppressSSL
public class TestExportTool extends SolrCloudTestCase {
+ @Test
public void testBasic() throws Exception {
String COLLECTION_NAME = "globalLoaderColl";
configureCluster(4).addConfig("conf", configset("cloud-dynamic")).configure();
@@ -218,6 +230,68 @@
}
}
+ @Test
+ public void testWithBasicAuth() throws Exception {
+ String COLLECTION_NAME = "secureCollection";
+ String USER = "solr";
+ String PASS = "SolrRocksAgain";
+ final String SECURITY_JSON =
+ Utils.toJSONString(
+ Map.of(
+ "authorization",
+ Map.of(
+ "class",
+ RuleBasedAuthorizationPlugin.class.getName(),
+ "user-role",
+ singletonMap(USER, "admin"),
+ "permissions",
+ singletonList(Map.of("name", "all", "role", "admin"))),
+ "authentication",
+ Map.of(
+ "class",
+ BasicAuthPlugin.class.getName(),
+ "blockUnknown",
+ true,
+ "credentials",
+ singletonMap(USER, getSaltedHashedValue(PASS)))));
+
+ configureCluster(2)
+ .addConfig("conf", configset("cloud-minimal"))
+ .withSecurityJson(SECURITY_JSON)
+ .configure();
+
+ try {
+ CollectionAdminRequest.createCollection(COLLECTION_NAME, "conf", 2, 1)
+ .setBasicAuthCredentials(USER, PASS)
+ .process(cluster.getSolrClient());
+ cluster.waitForActiveCollection(COLLECTION_NAME, 2, 2);
+
+ File outFile = File.createTempFile("output", ".json");
+
+ String[] args = {
+ "export",
+ "-url",
+ cluster.getJettySolrRunner(0).getBaseUrl() + "/" + COLLECTION_NAME,
+ "-credentials",
+ USER + ":" + PASS,
+ "-out",
+ outFile.getAbsolutePath(),
+ "-verbose"
+ };
+
+ assertEquals(0, runTool(args));
+ } finally {
+ cluster.shutdown();
+ }
+ }
+
+ private int runTool(String[] args) throws Exception {
+ Tool tool = findTool(args);
+ assertTrue(tool instanceof ExportTool);
+ CommandLine cli = parseCmdLine(tool.getName(), args, tool.getOptions());
+ return tool.runTool(cli);
+ }
+
private void assertJavabinDocsCount(ExportTool.Info info, int expected) throws IOException {
assertTrue(
"" + info.docsWritten.get() + " expected " + expected, info.docsWritten.get() >= expected);
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/CloudHttp2SolrClient.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/CloudHttp2SolrClient.java
index 9e4bdad..ffd481f 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/CloudHttp2SolrClient.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/CloudHttp2SolrClient.java
@@ -54,17 +54,12 @@
*/
protected CloudHttp2SolrClient(Builder builder) {
super(builder.shardLeadersOnly, builder.parallelUpdates, builder.directUpdatesToLeadersOnly);
- if (builder.httpClient == null) {
- this.clientIsInternal = true;
- if (builder.internalClientBuilder == null) {
- this.myClient = new Http2SolrClient.Builder().build();
- } else {
- this.myClient = builder.internalClientBuilder.build();
- }
- } else {
- this.clientIsInternal = false;
- this.myClient = builder.httpClient;
- }
+ this.clientIsInternal = builder.httpClient == null;
+ this.myClient = createOrGetHttpClientFromBuilder(builder);
+ this.stateProvider =
+ builder.zkHosts.isEmpty()
+ ? createHttp2ClusterStateProvider(builder.solrUrls, myClient)
+ : createZkClusterStateProvider(builder);
this.retryExpiryTimeNano = builder.retryExpiryTimeNano;
this.defaultCollection = builder.defaultCollection;
if (builder.requestWriter != null) {
@@ -73,7 +68,6 @@
if (builder.responseParser != null) {
this.myClient.setParser(builder.responseParser);
}
- this.stateProvider = builder.stateProvider;
this.collectionStateCache.timeToLiveMs =
TimeUnit.MILLISECONDS.convert(builder.timeToLiveSeconds, TimeUnit.SECONDS);
@@ -85,14 +79,64 @@
this.lbClient = new LBHttp2SolrClient.Builder(myClient).build();
}
+ private Http2SolrClient createOrGetHttpClientFromBuilder(Builder builder) {
+ if (builder.httpClient != null) {
+ return builder.httpClient;
+ } else if (builder.internalClientBuilder != null) {
+ return builder.internalClientBuilder.build();
+ } else {
+ return new Http2SolrClient.Builder().build();
+ }
+ }
+
+ private ClusterStateProvider createZkClusterStateProvider(Builder builder) {
+ try {
+ ClusterStateProvider stateProvider =
+ ClusterStateProvider.newZkClusterStateProvider(
+ builder.zkHosts, builder.zkChroot, builder.canUseZkACLs);
+ if (stateProvider instanceof SolrZkClientTimeoutAware) {
+ var timeoutAware = (SolrZkClientTimeoutAware) stateProvider;
+ timeoutAware.setZkClientTimeout(builder.zkClientTimeout);
+ timeoutAware.setZkConnectTimeout(builder.zkConnectTimeout);
+ }
+ return stateProvider;
+ } catch (Exception e) {
+ closeMyClientIfNeeded();
+ throw (e);
+ }
+ }
+
+ private ClusterStateProvider createHttp2ClusterStateProvider(
+ List<String> solrUrls, Http2SolrClient httpClient) {
+ try {
+ return new Http2ClusterStateProvider(solrUrls, httpClient);
+ } catch (Exception e) {
+ closeMyClientIfNeeded();
+ throw new RuntimeException(
+ "Couldn't initialize a HttpClusterStateProvider (is/are the "
+ + "Solr server(s), "
+ + solrUrls
+ + ", down?)",
+ e);
+ }
+ }
+
+ private void closeMyClientIfNeeded() {
+ try {
+ if (clientIsInternal && myClient != null) {
+ myClient.close();
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Exception on closing myClient", e);
+ }
+ }
+
@Override
public void close() throws IOException {
stateProvider.close();
lbClient.close();
- if (clientIsInternal && myClient != null) {
- myClient.close();
- }
+ closeMyClientIfNeeded();
super.close();
}
@@ -407,32 +451,11 @@
/** Create a {@link CloudHttp2SolrClient} based on the provided configuration. */
public CloudHttp2SolrClient build() {
- if (stateProvider == null) {
- if (!zkHosts.isEmpty() && !solrUrls.isEmpty()) {
- throw new IllegalArgumentException(
- "Both zkHost(s) & solrUrl(s) have been specified. Only specify one.");
- } else if (!zkHosts.isEmpty()) {
- stateProvider =
- ClusterStateProvider.newZkClusterStateProvider(zkHosts, zkChroot, canUseZkACLs);
- if (stateProvider instanceof SolrZkClientTimeoutAware) {
- var timeoutAware = (SolrZkClientTimeoutAware) stateProvider;
- timeoutAware.setZkClientTimeout(zkClientTimeout);
- timeoutAware.setZkConnectTimeout(zkConnectTimeout);
- }
- } else if (!solrUrls.isEmpty()) {
- try {
- stateProvider = new Http2ClusterStateProvider(solrUrls, httpClient);
- } catch (Exception e) {
- throw new RuntimeException(
- "Couldn't initialize a HttpClusterStateProvider (is/are the "
- + "Solr server(s), "
- + solrUrls
- + ", down?)",
- e);
- }
- } else {
- throw new IllegalArgumentException("Both zkHosts and solrUrl cannot be null.");
- }
+ if (!zkHosts.isEmpty() && !solrUrls.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Both zkHost(s) & solrUrl(s) have been specified. Only specify one.");
+ } else if (zkHosts.isEmpty() && solrUrls.isEmpty()) {
+ throw new IllegalArgumentException("Both zkHosts and solrUrl cannot be null.");
}
return new CloudHttp2SolrClient(this);
}
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/Http2ClusterStateProvider.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/Http2ClusterStateProvider.java
index f46fe1a..3bb222a 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/Http2ClusterStateProvider.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/Http2ClusterStateProvider.java
@@ -43,4 +43,8 @@
protected SolrClient getSolrClient(String baseUrl) {
return new Http2SolrClient.Builder(baseUrl).withHttpClient(httpClient).build();
}
+
+ public Http2SolrClient getHttpClient() {
+ return httpClient;
+ }
}
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudHttp2SolrClientBuilderTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudHttp2SolrClientBuilderTest.java
index b776755..19c32d9 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudHttp2SolrClientBuilderTest.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudHttp2SolrClientBuilderTest.java
@@ -17,7 +17,6 @@
package org.apache.solr.client.solrj.impl;
-import static org.apache.solr.SolrTestCaseJ4.assumeWorkingMockito;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -28,15 +27,31 @@
import java.util.Collections;
import java.util.List;
import java.util.Optional;
-import org.apache.solr.SolrTestCase;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.Mockito;
-public class CloudHttp2SolrClientBuilderTest extends SolrTestCase {
+public class CloudHttp2SolrClientBuilderTest extends SolrCloudTestCase {
private static final String ANY_CHROOT = "/ANY_CHROOT";
private static final String ANY_ZK_HOST = "ANY_ZK_HOST";
private static final String ANY_OTHER_ZK_HOST = "ANY_OTHER_ZK_HOST";
+ @BeforeClass
+ public static void setupCluster() throws Exception {
+ assumeWorkingMockito();
+ configureCluster(1)
+ .addConfig(
+ "conf",
+ getFile("solrj")
+ .toPath()
+ .resolve("solr")
+ .resolve("configsets")
+ .resolve("configset-1")
+ .resolve("conf"))
+ .configure();
+ }
+
@Test
public void testSingleZkHostSpecified() throws IOException {
try (CloudSolrClient createdClient =
@@ -108,7 +123,6 @@
@Test
public void testExternalClientAndInternalBuilderTogether() {
- assumeWorkingMockito();
expectThrows(
IllegalStateException.class,
() ->
@@ -129,7 +143,6 @@
@Test
public void testProvideInternalBuilder() throws IOException {
- assumeWorkingMockito();
Http2SolrClient http2Client = Mockito.mock(Http2SolrClient.class);
Http2SolrClient.Builder http2ClientBuilder = Mockito.mock(Http2SolrClient.Builder.class);
when(http2ClientBuilder.build()).thenReturn(http2Client);
@@ -149,7 +162,6 @@
@Test
public void testProvideExternalClient() throws IOException {
- assumeWorkingMockito();
Http2SolrClient http2Client = Mockito.mock(Http2SolrClient.class);
CloudHttp2SolrClient.Builder clientBuilder =
new CloudHttp2SolrClient.Builder(
@@ -172,4 +184,48 @@
assertEquals("aCollection", createdClient.getDefaultCollection());
}
}
+
+ /**
+ * Tests the consistency between the HTTP client used by {@link CloudHttp2SolrClient} and the one
+ * used by its associated {@link Http2ClusterStateProvider}. This method ensures that whether a
+ * {@link CloudHttp2SolrClient} is created with a specific HTTP client, an internal client
+ * builder, or with no specific HTTP client at all, the same HTTP client instance is used both by
+ * the {@link CloudHttp2SolrClient} and by its {@link Http2ClusterStateProvider}.
+ */
+ @Test
+ public void testHttpClientPreservedInHttp2ClusterStateProvider() throws IOException {
+ List<String> solrUrls = List.of(cluster.getJettySolrRunner(0).getBaseUrl().toString());
+
+ // No httpClient - No internalClientBuilder
+ testHttpClientConsistency(solrUrls, null, null);
+
+ // httpClient - No internalClientBuilder
+ try (Http2SolrClient httpClient = new Http2SolrClient.Builder().build()) {
+ testHttpClientConsistency(solrUrls, httpClient, null);
+ }
+
+ // No httpClient - internalClientBuilder
+ Http2SolrClient.Builder internalClientBuilder = new Http2SolrClient.Builder();
+ testHttpClientConsistency(solrUrls, null, internalClientBuilder);
+ }
+
+ private void testHttpClientConsistency(
+ List<String> solrUrls,
+ Http2SolrClient httpClient,
+ Http2SolrClient.Builder internalClientBuilder)
+ throws IOException {
+ CloudHttp2SolrClient.Builder clientBuilder = new CloudHttp2SolrClient.Builder(solrUrls);
+
+ if (httpClient != null) {
+ clientBuilder.withHttpClient(httpClient);
+ } else if (internalClientBuilder != null) {
+ clientBuilder.withInternalClientBuilder(internalClientBuilder);
+ }
+
+ try (CloudHttp2SolrClient client = clientBuilder.build()) {
+ assertEquals(
+ client.getHttpClient(),
+ ((Http2ClusterStateProvider) client.getClusterStateProvider()).getHttpClient());
+ }
+ }
}