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 2026/03/30 03:18:02 UTC
(solr) 02/03: SOLR-18150: Add SolrBackend abstraction for test/benchmark deployments (#4214)
This is an automated email from the ASF dual-hosted git repository.
dsmiley pushed a commit to branch branch_10x
in repository https://gitbox.apache.org/repos/asf/solr.git
commit af9527b25566f532dae480fe521c90fc20de6ca9
Author: David Smiley <ds...@apache.org>
AuthorDate: Sun Mar 29 11:23:13 2026 -0400
SOLR-18150: Add SolrBackend abstraction for test/benchmark deployments (#4214)
SOLR-18150: Add SolrBackend abstraction for test/benchmark deployments
Created SolrBackend interface to abstract over different Solr deployment
types (embedded, remote, Jetty, MiniSolrCloudCluster) with a unified API
for collection/configSet management, diagnostics, and node URL access.
JettySolrRunner now manages a HttpJettySolrClient instance, get via getSolrClient.
This PR only introduces the abstraction; follow-on PRs will add callers
in benchmarks and tests.
Co-authored-by: Claude Opus 4.6 <no...@anthropic.com>
---
changelog/unreleased/SOLR-18150-SolrBackend.yml | 7 +
.../java/org/apache/solr/bench/SolrBenchState.java | 9 +-
.../solr/cloud/RecoveryStrategyStressTest.java | 3 +-
.../test/org/apache/solr/cloud/RecoveryZkTest.java | 3 +-
.../apache/solr/cloud/RecoveryZkTestWithAuth.java | 3 +-
.../solr/search/facet/RangeFacetCloudTest.java | 2 +-
.../search/facet/TestCloudJSONFacetJoinDomain.java | 2 +-
.../solr/search/facet/TestCloudJSONFacetSKG.java | 2 +-
.../search/facet/TestCloudJSONFacetSKGEquiv.java | 2 +-
.../solr/search/join/TestCloudNestedDocsSort.java | 2 +-
.../solr/servlet/TestRequestRateLimiter.java | 6 +-
.../solr/update/DeleteByIdWithRouterFieldTest.java | 2 +-
.../examples/IndexingNestedDocuments.java | 6 +-
.../src/java/org/apache/solr/SolrBackend.java | 168 +++++++++++++++++++++
.../apache/solr/cloud/MiniSolrCloudCluster.java | 136 +++++++++--------
.../org/apache/solr/cloud/RemoteSolrBackend.java | 119 +++++++++++++++
.../apache/solr/embedded/EmbeddedSolrBackend.java | 100 ++++++++++++
.../org/apache/solr/embedded/JettySolrRunner.java | 96 +++++++++---
18 files changed, 563 insertions(+), 105 deletions(-)
diff --git a/changelog/unreleased/SOLR-18150-SolrBackend.yml b/changelog/unreleased/SOLR-18150-SolrBackend.yml
new file mode 100644
index 00000000000..eccb8cedaa0
--- /dev/null
+++ b/changelog/unreleased/SOLR-18150-SolrBackend.yml
@@ -0,0 +1,7 @@
+title: 'solr-test-framework: New SolrBackend abstraction for use by solr/benchmark and SolrClientTestRule. Useful for anyone using other test frameworks or higher JUnit versions.'
+type: other
+authors:
+ - name: David Smiley
+links:
+ - name: SOLR-18150
+ url: https://issues.apache.org/jira/browse/SOLR-18150
diff --git a/solr/benchmark/src/java/org/apache/solr/bench/SolrBenchState.java b/solr/benchmark/src/java/org/apache/solr/bench/SolrBenchState.java
index 0a7e9b1f2be..ff6a09cfe51 100755
--- a/solr/benchmark/src/java/org/apache/solr/bench/SolrBenchState.java
+++ b/solr/benchmark/src/java/org/apache/solr/bench/SolrBenchState.java
@@ -25,6 +25,7 @@ import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.management.ManagementFactory;
import java.net.URL;
+import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.Files;
@@ -127,7 +128,10 @@ public class SolrBenchState {
benchmarkParams.getBenchmark() + ".txt");
Files.createDirectories(metricsResults.getParent());
- cluster.dumpMetrics(metricsResults.getParent(), metricsResults.getFileName().toString());
+ try (var out =
+ new PrintStream(Files.newOutputStream(metricsResults), false, Charset.defaultCharset())) {
+ cluster.dumpMetrics(out);
+ }
}
/**
@@ -346,7 +350,6 @@ public class SolrBenchState {
this.useHttp1 = useHttp1;
}
- @SuppressForbidden(reason = "This module does not need to deal with logging context")
public void index(String collection, Docs docs, int docCount) throws Exception {
index(collection, docs, docCount, true);
}
@@ -390,7 +393,7 @@ public class SolrBenchState {
dumpCoreInfo();
}
- @SuppressForbidden(reason = "This module does not need to deal with logging context")
+ @SuppressForbidden(reason = "Benchmarks may use JDK ExecutorService impls")
private void indexParallel(String collection, Docs docs, int docCount)
throws InterruptedException {
Meter meter = new Meter();
diff --git a/solr/core/src/test/org/apache/solr/cloud/RecoveryStrategyStressTest.java b/solr/core/src/test/org/apache/solr/cloud/RecoveryStrategyStressTest.java
index 9e66bee3867..2cd0ef45a34 100644
--- a/solr/core/src/test/org/apache/solr/cloud/RecoveryStrategyStressTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/RecoveryStrategyStressTest.java
@@ -54,8 +54,7 @@ public class RecoveryStrategyStressTest extends SolrCloudTestCase {
"Expected a collection with one shard and two replicas", collection, clusterShape(1, 4));
final var scheduledExecutorService =
Executors.newScheduledThreadPool(1, new SolrNamedThreadFactory("stressTestRecovery"));
- try (SolrClient solrClient =
- cluster.basicSolrClientBuilder().withDefaultCollection(collection).build()) {
+ try (SolrClient solrClient = cluster.newSolrClient(collection)) {
final StoppableIndexingThread indexThread =
new StoppableIndexingThread(null, solrClient, "1", true, 10, 1, true);
diff --git a/solr/core/src/test/org/apache/solr/cloud/RecoveryZkTest.java b/solr/core/src/test/org/apache/solr/cloud/RecoveryZkTest.java
index 5ea4794ff4b..48d4855c1d2 100644
--- a/solr/core/src/test/org/apache/solr/cloud/RecoveryZkTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/RecoveryZkTest.java
@@ -81,8 +81,7 @@ public class RecoveryZkTest extends SolrCloudTestCase {
}
log.info("Indexing {} documents", maxDoc);
- try (SolrClient solrClient =
- cluster.basicSolrClientBuilder().withDefaultCollection(collection).build(); ) {
+ try (SolrClient solrClient = cluster.newSolrClient(collection); ) {
final StoppableIndexingThread indexThread =
new StoppableIndexingThread(null, solrClient, "1", true, maxDoc, 1, true);
threads.add(indexThread);
diff --git a/solr/core/src/test/org/apache/solr/cloud/RecoveryZkTestWithAuth.java b/solr/core/src/test/org/apache/solr/cloud/RecoveryZkTestWithAuth.java
index 3a62ac08f88..479a5c44ba3 100644
--- a/solr/core/src/test/org/apache/solr/cloud/RecoveryZkTestWithAuth.java
+++ b/solr/core/src/test/org/apache/solr/cloud/RecoveryZkTestWithAuth.java
@@ -78,8 +78,7 @@ public class RecoveryZkTestWithAuth extends SolrCloudTestCase {
.process(cluster.getSolrClient());
waitForState(
"Expected a collection with one shard and one replicas", collection, clusterShape(1, 1));
- try (SolrClient solrClient =
- cluster.basicSolrClientBuilder().withDefaultCollection(collection).build()) {
+ try (SolrClient solrClient = cluster.newSolrClient(collection)) {
UpdateRequest commitReq = new UpdateRequest();
withBasicAuth(commitReq);
for (int i = 0; i < 500; i++) {
diff --git a/solr/core/src/test/org/apache/solr/search/facet/RangeFacetCloudTest.java b/solr/core/src/test/org/apache/solr/search/facet/RangeFacetCloudTest.java
index fbb9abb1b33..697a2f8b680 100644
--- a/solr/core/src/test/org/apache/solr/search/facet/RangeFacetCloudTest.java
+++ b/solr/core/src/test/org/apache/solr/search/facet/RangeFacetCloudTest.java
@@ -102,7 +102,7 @@ public class RangeFacetCloudTest extends SolrCloudTestCase {
.process(cluster.getSolrClient()))
.getStatus());
- CLOUD_CLIENT = cluster.basicSolrClientBuilder().withDefaultCollection(COLLECTION).build();
+ CLOUD_CLIENT = cluster.newSolrClient(COLLECTION);
final int numDocs = atLeast(1000);
final int maxTermId = atLeast(TERM_VALUES_RANDOMIZER);
diff --git a/solr/core/src/test/org/apache/solr/search/facet/TestCloudJSONFacetJoinDomain.java b/solr/core/src/test/org/apache/solr/search/facet/TestCloudJSONFacetJoinDomain.java
index 93e7f07bf4c..2853d1c7c3f 100644
--- a/solr/core/src/test/org/apache/solr/search/facet/TestCloudJSONFacetJoinDomain.java
+++ b/solr/core/src/test/org/apache/solr/search/facet/TestCloudJSONFacetJoinDomain.java
@@ -118,7 +118,7 @@ public class TestCloudJSONFacetJoinDomain extends SolrCloudTestCase {
.setProperties(collectionProperties)
.process(cluster.getSolrClient());
- CLOUD_CLIENT = cluster.basicSolrClientBuilder().withDefaultCollection(COLLECTION_NAME).build();
+ CLOUD_CLIENT = cluster.newSolrClient(COLLECTION_NAME);
waitForRecoveriesToFinish(CLOUD_CLIENT);
diff --git a/solr/core/src/test/org/apache/solr/search/facet/TestCloudJSONFacetSKG.java b/solr/core/src/test/org/apache/solr/search/facet/TestCloudJSONFacetSKG.java
index c6e9dfa0094..48ccb4a133f 100644
--- a/solr/core/src/test/org/apache/solr/search/facet/TestCloudJSONFacetSKG.java
+++ b/solr/core/src/test/org/apache/solr/search/facet/TestCloudJSONFacetSKG.java
@@ -145,7 +145,7 @@ public class TestCloudJSONFacetSKG extends SolrCloudTestCase {
.setProperties(collectionProperties)
.process(cluster.getSolrClient());
- CLOUD_CLIENT = cluster.basicSolrClientBuilder().withDefaultCollection(COLLECTION_NAME).build();
+ CLOUD_CLIENT = cluster.newSolrClient(COLLECTION_NAME);
waitForRecoveriesToFinish(CLOUD_CLIENT);
diff --git a/solr/core/src/test/org/apache/solr/search/facet/TestCloudJSONFacetSKGEquiv.java b/solr/core/src/test/org/apache/solr/search/facet/TestCloudJSONFacetSKGEquiv.java
index b295fd4c9a3..087f2a2ec5c 100644
--- a/solr/core/src/test/org/apache/solr/search/facet/TestCloudJSONFacetSKGEquiv.java
+++ b/solr/core/src/test/org/apache/solr/search/facet/TestCloudJSONFacetSKGEquiv.java
@@ -137,7 +137,7 @@ public class TestCloudJSONFacetSKGEquiv extends SolrCloudTestCase {
.setProperties(collectionProperties)
.process(cluster.getSolrClient());
- CLOUD_CLIENT = cluster.basicSolrClientBuilder().withDefaultCollection(COLLECTION_NAME).build();
+ CLOUD_CLIENT = cluster.newSolrClient(COLLECTION_NAME);
waitForRecoveriesToFinish(CLOUD_CLIENT);
diff --git a/solr/core/src/test/org/apache/solr/search/join/TestCloudNestedDocsSort.java b/solr/core/src/test/org/apache/solr/search/join/TestCloudNestedDocsSort.java
index 35fc3ffbfbd..9921394e136 100644
--- a/solr/core/src/test/org/apache/solr/search/join/TestCloudNestedDocsSort.java
+++ b/solr/core/src/test/org/apache/solr/search/join/TestCloudNestedDocsSort.java
@@ -73,7 +73,7 @@ public class TestCloudNestedDocsSort extends SolrCloudTestCase {
.withProperty("schema", "schema.xml")
.process(cluster.getSolrClient());
- client = cluster.basicSolrClientBuilder().withDefaultCollection("collection1").build();
+ client = cluster.newSolrClient("collection1");
ZkStateReader zkStateReader = ZkStateReader.from(client);
AbstractFullDistribZkTestBase.waitForRecoveriesToFinish(
diff --git a/solr/core/src/test/org/apache/solr/servlet/TestRequestRateLimiter.java b/solr/core/src/test/org/apache/solr/servlet/TestRequestRateLimiter.java
index 1c7c49b1c41..d551862227c 100644
--- a/solr/core/src/test/org/apache/solr/servlet/TestRequestRateLimiter.java
+++ b/solr/core/src/test/org/apache/solr/servlet/TestRequestRateLimiter.java
@@ -67,8 +67,7 @@ public class TestRequestRateLimiter extends SolrCloudTestCase {
@Test
public void testConcurrentQueries() throws Exception {
- try (CloudSolrClient client =
- cluster.basicSolrClientBuilder().withDefaultCollection(FIRST_COLLECTION).build()) {
+ try (CloudSolrClient client = cluster.newSolrClient(FIRST_COLLECTION)) {
CollectionAdminRequest.createCollection(FIRST_COLLECTION, 1, 1).process(client);
cluster.waitForActiveCollection(FIRST_COLLECTION, 1, 1);
@@ -287,8 +286,7 @@ public class TestRequestRateLimiter extends SolrCloudTestCase {
@Nightly
public void testSlotBorrowing() throws Exception {
- try (CloudSolrClient client =
- cluster.basicSolrClientBuilder().withDefaultCollection(SECOND_COLLECTION).build()) {
+ try (CloudSolrClient client = cluster.newSolrClient(SECOND_COLLECTION)) {
CollectionAdminRequest.createCollection(SECOND_COLLECTION, 1, 1).process(client);
cluster.waitForActiveCollection(SECOND_COLLECTION, 1, 1);
diff --git a/solr/core/src/test/org/apache/solr/update/DeleteByIdWithRouterFieldTest.java b/solr/core/src/test/org/apache/solr/update/DeleteByIdWithRouterFieldTest.java
index a36b7b02cd6..cf5ead4b685 100644
--- a/solr/core/src/test/org/apache/solr/update/DeleteByIdWithRouterFieldTest.java
+++ b/solr/core/src/test/org/apache/solr/update/DeleteByIdWithRouterFieldTest.java
@@ -78,7 +78,7 @@ public class DeleteByIdWithRouterFieldTest extends SolrCloudTestCase {
.process(cluster.getSolrClient())
.isSuccess());
- solrClient = cluster.basicSolrClientBuilder().withDefaultCollection(COLL).build();
+ solrClient = cluster.newSolrClient(COLL);
ClusterState clusterState = cluster.getSolrClient().getClusterState();
for (Replica replica : clusterState.getCollection(COLL).getReplicas()) {
diff --git a/solr/solr-ref-guide/modules/indexing-guide/examples/IndexingNestedDocuments.java b/solr/solr-ref-guide/modules/indexing-guide/examples/IndexingNestedDocuments.java
index 1310d65185d..668abf3d592 100644
--- a/solr/solr-ref-guide/modules/indexing-guide/examples/IndexingNestedDocuments.java
+++ b/solr/solr-ref-guide/modules/indexing-guide/examples/IndexingNestedDocuments.java
@@ -73,8 +73,7 @@ public class IndexingNestedDocuments extends SolrCloudTestCase {
.process(cluster.getSolrClient());
// configure the client with the default collection name, to simplify our example below.
- IndexingNestedDocuments.clientUsedInSolrJExample =
- cluster.basicSolrClientBuilder().withDefaultCollection(collection).build();
+ IndexingNestedDocuments.clientUsedInSolrJExample = cluster.newSolrClient(collection);
//
// DO NOT MODIFY THESE EXAMPLE DOCS WITHOUT MAKING THE SAME CHANGES TO THE JSON AND XML
@@ -165,8 +164,7 @@ public class IndexingNestedDocuments extends SolrCloudTestCase {
CollectionAdminRequest.createCollection(collection, 1, 1).process(cluster.getSolrClient());
// configure the client with the default collection name, to simplify our example below.
- IndexingNestedDocuments.clientUsedInSolrJExample =
- cluster.basicSolrClientBuilder().withDefaultCollection(collection).build();
+ IndexingNestedDocuments.clientUsedInSolrJExample = cluster.newSolrClient(collection);
//
// DO NOT MODIFY THESE EXAMPLE DOCS WITHOUT MAKING THE SAME CHANGES TO THE JSON AND XML
diff --git a/solr/test-framework/src/java/org/apache/solr/SolrBackend.java b/solr/test-framework/src/java/org/apache/solr/SolrBackend.java
new file mode 100644
index 00000000000..db6c3f5ad09
--- /dev/null
+++ b/solr/test-framework/src/java/org/apache/solr/SolrBackend.java
@@ -0,0 +1,168 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.file.Path;
+import java.util.Random;
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.impl.CloudSolrClient;
+import org.apache.solr.client.solrj.impl.HttpSolrClientBase;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.GenericSolrRequest;
+import org.apache.solr.client.solrj.request.MetricsRequest;
+import org.apache.solr.client.solrj.response.InputStreamResponseParser;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.URLUtil;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.embedded.EmbeddedSolrBackend;
+import org.apache.solr.embedded.JettySolrRunner;
+
+/**
+ * Abstraction over a running Solr deployment for use in tests and benchmarks. The abstraction
+ * normalizes how to shut down and perform other common operations as the methods indicate.
+ */
+public interface SolrBackend extends AutoCloseable {
+
+ /**
+ * Creates a new {@link SolrClient} defaulted to the given collection. The <em>caller</em> owns
+ * this client and is responsible for closing it. Callers that want a long-lived client should
+ * cache it themselves.
+ */
+ SolrClient newSolrClient(String collection);
+
+ /**
+ * Returns the node / admin (collection-less) {@link SolrClient} owned by this backend. The caller
+ * must NOT close it; it is released when this backend is {@link #close()}d. A {@link
+ * CloudSolrClient} should be returned if appropriate. While it *can* be used to target specific
+ * collections, please use {@link #newSolrClient(String)} for that instead.
+ */
+ SolrClient getSolrClient();
+
+ /**
+ * Upload a configSet, possibly overwriting (creating files, updating files, NOT deleting files).
+ *
+ * @param configDir a path to the config set to upload
+ * @param name the name to give the configSet
+ */
+ default void uploadConfigSet(Path configDir, String name)
+ throws SolrServerException, IOException {
+ getCoreContainer().getConfigSetService().uploadConfig(name, configDir);
+ }
+
+ /**
+ * Checks if a configSet with the given name exists.
+ *
+ * @param name configSet name to check
+ * @return true if the configSet exists, false otherwise
+ */
+ default boolean hasConfigSet(String name) throws SolrServerException, IOException {
+ return getCoreContainer().getConfigSetService().checkConfigExists(name);
+ }
+
+ /**
+ * Creates a collection (or core for single-node backends). Cloud backends honour {@code
+ * numShards} and {@code replicationFactor}; single-node backends ({@link JettySolrRunner}, {@link
+ * EmbeddedSolrBackend}) ignore those fields. Callers should use {@link #hasCollection(String)}
+ * first if they want to avoid errors when the collection already exists. Tests/benchmarks that
+ * want to test how this works should not use this to do so.
+ */
+ void createCollection(CollectionAdminRequest.Create create)
+ throws SolrServerException, IOException;
+
+ /**
+ * Checks if a collection or core with the given name exists.
+ *
+ * @param name collection or core name to check
+ * @return true if the collection/core exists, false otherwise
+ */
+ boolean hasCollection(String name) throws SolrServerException, IOException;
+
+ /** Reloads a collection or core by this name. The purpose is typically to clear caches. */
+ default void reloadCollection(String name) throws SolrServerException, IOException {
+ getSolrClient().request(CollectionAdminRequest.reloadCollection(name));
+ }
+
+ /**
+ * Provides access to an embedded/in-process {@link org.apache.solr.core.CoreContainer} -- if
+ * available (else null). If there is more than one node, then one is returned.
+ *
+ * @return can be null.
+ */
+ CoreContainer getCoreContainer();
+
+ /**
+ * Returns the base URL of a Solr node. For cloud backends with multiple nodes, a live node is
+ * chosen at random. The URL does not include a trailing slash.
+ *
+ * @return base URL (e.g., "http://localhost:8983/solr"). Null for EmbeddedSolrServer.
+ */
+ default String getBaseUrl(Random r) {
+ // Get live nodes and pick one randomly
+ SolrClient adminClient = getSolrClient();
+ if (adminClient instanceof HttpSolrClientBase httpSolrClient) {
+ return httpSolrClient.getBaseURL();
+ } else if (adminClient instanceof CloudSolrClient cloudClient) {
+ var liveNodes = cloudClient.getClusterStateProvider().getLiveNodes();
+ if (liveNodes.isEmpty()) {
+ return null;
+ }
+ String randomNode =
+ liveNodes.stream().skip(r.nextInt(liveNodes.size())).findFirst().orElseThrow();
+ String urlScheme = cloudClient.getClusterStateProvider().getUrlScheme();
+ return URLUtil.getBaseUrlForNodeName(randomNode, urlScheme);
+ } else {
+ return null;
+ }
+ }
+
+ /** Dumps Prometheus-format metrics to {@code out}. No-op is an acceptable implementation. */
+ default void dumpMetrics(PrintStream out) {
+ try {
+ var request = new MetricsRequest();
+ request.setResponseParser(new InputStreamResponseParser("prometheus"));
+ var response = request.process(getSolrClient());
+ out.println(InputStreamResponseParser.consumeResponseToString(response.getResponse()));
+ } catch (SolrServerException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Dumps JSON-format information for all cores to {@code out}. No-op is an acceptable
+ * implementation. Default uses {@code GET /admin/cores?indexInfo=true}.
+ */
+ default void dumpCoreInfo(PrintStream out) {
+ try {
+ var request =
+ new GenericSolrRequest(
+ SolrRequest.METHOD.GET, "/admin/cores", SolrParams.of("indexInfo", "true"));
+ request.setResponseParser(new InputStreamResponseParser("json"));
+ var response = request.process(getSolrClient());
+ out.println(InputStreamResponseParser.consumeResponseToString(response.getResponse()));
+ } catch (SolrServerException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Shuts down this backend, releases the admin client, and all other resources. */
+ @Override
+ void close();
+}
diff --git a/solr/test-framework/src/java/org/apache/solr/cloud/MiniSolrCloudCluster.java b/solr/test-framework/src/java/org/apache/solr/cloud/MiniSolrCloudCluster.java
index ee8ced5a367..f2bbfb87f9b 100644
--- a/solr/test-framework/src/java/org/apache/solr/cloud/MiniSolrCloudCluster.java
+++ b/solr/test-framework/src/java/org/apache/solr/cloud/MiniSolrCloudCluster.java
@@ -16,8 +16,6 @@
*/
package org.apache.solr.cloud;
-import static org.apache.solr.core.ConfigSetProperties.DEFAULT_FILENAME;
-
import com.codahale.metrics.MetricRegistry;
import io.dropwizard.metrics.jetty12.ee10.InstrumentedEE10Handler;
import jakarta.servlet.Filter;
@@ -55,12 +53,13 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import org.apache.lucene.tests.util.LuceneTestCase;
+import org.apache.solr.SolrBackend;
import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.apache.CloudLegacySolrClient;
import org.apache.solr.client.solrj.impl.CloudSolrClient;
import org.apache.solr.client.solrj.jetty.SSLConfig;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
-import org.apache.solr.client.solrj.request.ConfigSetAdminRequest;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.cloud.Aliases;
@@ -90,7 +89,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** "Mini" SolrCloud cluster to be used for testing */
-public class MiniSolrCloudCluster {
+public class MiniSolrCloudCluster implements SolrBackend {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final URL PRE_GENERATED_PRIVATE_KEY_URL =
@@ -334,7 +333,7 @@ public class MiniSolrCloudCluster {
throw startupError;
}
- solrClient = buildSolrClient();
+ solrClient = newSolrClient(null);
if (numServers > 0) {
waitForAllNodes(numServers, 60);
@@ -564,23 +563,6 @@ public class MiniSolrCloudCluster {
return jetty;
}
- /**
- * Upload a config set
- *
- * @param configDir a path to the config set to upload
- * @param configName the name to give the configset
- */
- public void uploadConfigSet(Path configDir, String configName) throws IOException {
- try (SolrZkClient zkClient =
- new SolrZkClient.Builder()
- .withUrl(zkServer.getZkAddress())
- .withTimeout(AbstractZkTestCase.TIMEOUT, TimeUnit.MILLISECONDS)
- .withConnTimeOut(AbstractZkTestCase.TIMEOUT, TimeUnit.MILLISECONDS)
- .build()) {
- new ZkConfigSetService(zkClient).uploadConfig(configName, configDir);
- }
- }
-
/** Delete all collections (and aliases) */
public void deleteAllCollections() throws Exception {
try (ZkStateReader reader = new ZkStateReader(getZkClient())) {
@@ -632,22 +614,32 @@ public class MiniSolrCloudCluster {
}
public void deleteAllConfigSets() throws Exception {
-
- List<String> configSetNames =
- new ConfigSetAdminRequest.List().process(solrClient).getConfigSets();
-
- for (String configSet : configSetNames) {
+ ZkConfigSetService service = new ZkConfigSetService(getZkClient());
+ for (String configSet : service.listConfigs()) {
if (configSet.equals("_default")) {
continue;
}
- try {
- // cleanup any property before removing the configset
- getZkClient()
- .delete(
- ZkConfigSetService.CONFIGS_ZKNODE + "/" + configSet + "/" + DEFAULT_FILENAME, -1);
- } catch (KeeperException.NoNodeException nne) {
- }
- new ConfigSetAdminRequest.Delete().setConfigSetName(configSet).process(solrClient);
+ service.deleteConfig(configSet);
+ }
+ }
+
+ @Override
+ public void uploadConfigSet(Path configDir, String configName) throws IOException {
+ new ZkConfigSetService(getZkClient()).uploadConfig(configName, configDir);
+ }
+
+ @Override
+ public boolean hasConfigSet(String name) throws IOException {
+ return new ZkConfigSetService(getZkClient()).checkConfigExists(name);
+ }
+
+ /** Shut down the cluster, including all Solr nodes and ZooKeeper. Doesn't throw. */
+ @Override
+ public void close() {
+ try {
+ shutdown();
+ } catch (Exception e) { // don't propagate; more noisy than helpful
+ log.error(e.toString(), e); // nowarn
}
}
@@ -690,6 +682,7 @@ public class MiniSolrCloudCluster {
return baseDir;
}
+ @Override // SolrBackend
public CloudSolrClient getSolrClient() {
return solrClient;
}
@@ -725,6 +718,16 @@ public class MiniSolrCloudCluster {
});
}
+ @Override // SolrBackend
+ public CloudSolrClient newSolrClient(String collection) {
+ return new CloudLegacySolrClient.Builder(
+ Collections.singletonList(getZkServer().getZkAddress()), Optional.empty())
+ .withSocketTimeout(90000, TimeUnit.MILLISECONDS)
+ .withConnectionTimeout(15000, TimeUnit.MILLISECONDS)
+ .withDefaultCollection(collection)
+ .build(); // we choose 90 because we run in some harsh envs
+ }
+
public SolrZkClient getZkClient() {
return getZkStateReader().getZkClient();
}
@@ -741,27 +744,6 @@ public class MiniSolrCloudCluster {
}
}
- protected CloudSolrClient buildSolrClient() {
- return new CloudLegacySolrClient.Builder(
- Collections.singletonList(getZkServer().getZkAddress()), Optional.empty())
- .withSocketTimeout(90000, TimeUnit.MILLISECONDS)
- .withConnectionTimeout(15000, TimeUnit.MILLISECONDS)
- .build(); // we choose 90 because we run in some harsh envs
- }
-
- /**
- * creates a basic CloudSolrClient Builder that then can be customized by callers, for example by
- * specifying what collection they want to use.
- *
- * @return CloudLegacySolrClient.Builder
- */
- public CloudLegacySolrClient.Builder basicSolrClientBuilder() {
- return new CloudLegacySolrClient.Builder(
- Collections.singletonList(getZkServer().getZkAddress()), Optional.empty())
- .withSocketTimeout(90000) // we choose 90 because we run in some harsh envs
- .withConnectionTimeout(15000);
- }
-
private Exception checkForExceptions(String message, Collection<Future<JettySolrRunner>> futures)
throws InterruptedException {
Exception parsed = new Exception(message);
@@ -820,6 +802,26 @@ public class MiniSolrCloudCluster {
throw new SolrException(ErrorCode.NOT_FOUND, "No open Overseer found");
}
+ @Override // SolrBackend
+ public CoreContainer getCoreContainer() {
+ return jettys.size() != 1 ? null : jettys.getFirst().getCoreContainer();
+ }
+
+ @Override // SolrBackend
+ public void createCollection(CollectionAdminRequest.Create create)
+ throws SolrServerException, IOException {
+ String collectionName = create.getCollectionName();
+ create.process(this.getSolrClient());
+ int shards = create.getNumShards() != null ? create.getNumShards() : 1;
+ int replicas = create.getReplicationFactor() != null ? create.getReplicationFactor() : 1;
+ waitForActiveCollection(collectionName, 15, TimeUnit.SECONDS, shards, shards * replicas);
+ }
+
+ @Override // SolrBackend
+ public boolean hasCollection(String name) {
+ return getZkStateReader().getClusterState().hasCollection(name);
+ }
+
public void waitForActiveCollection(
String collection, long wait, TimeUnit unit, int shards, int totalReplicas) {
log.info("waitForActiveCollection: {}", collection);
@@ -938,15 +940,27 @@ public class MiniSolrCloudCluster {
}
}
- public void dumpMetrics(Path outputDirectory, String fileName) throws IOException {
- for (JettySolrRunner jetty : jettys) {
- jetty.outputMetrics(outputDirectory, fileName);
+ @Override
+ public void dumpMetrics(PrintStream out) {
+ try {
+ for (JettySolrRunner jetty : jettys) {
+ out.println();
+ out.println("# Jetty " + jetty.getNodeName() + " metrics:");
+ out.println();
+ jetty.outputMetrics(out);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
}
}
- public void dumpCoreInfo(PrintStream pw) throws IOException {
+ @Override
+ public void dumpCoreInfo(PrintStream out) {
for (JettySolrRunner jetty : jettys) {
- jetty.dumpCoresInfo(pw);
+ out.println();
+ out.println(" Jetty " + jetty.getNodeName() + " cores:");
+ out.println();
+ jetty.dumpCoresInfo(out);
}
}
diff --git a/solr/test-framework/src/java/org/apache/solr/cloud/RemoteSolrBackend.java b/solr/test-framework/src/java/org/apache/solr/cloud/RemoteSolrBackend.java
new file mode 100644
index 00000000000..78cb06ecee0
--- /dev/null
+++ b/solr/test-framework/src/java/org/apache/solr/cloud/RemoteSolrBackend.java
@@ -0,0 +1,119 @@
+/*
+ * 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.cloud;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+import org.apache.solr.SolrBackend;
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.impl.CloudSolrClient;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.ConfigSetAdminRequest;
+import org.apache.solr.common.util.IOUtils;
+import org.apache.solr.common.util.URLUtil;
+import org.apache.solr.core.CoreContainer;
+
+/**
+ * {@link SolrBackend} that connects to a pre-existing remote SolrCloud cluster. The caller supplies
+ * the HTTP connection string at construction time (e.g. {@code http://localhost:8983/solr}).
+ */
+public class RemoteSolrBackend implements SolrBackend {
+
+ private final CloudSolrClient adminClient;
+
+ public RemoteSolrBackend(String baseUrl) {
+ this.adminClient = new CloudSolrClient.Builder(List.of(baseUrl)).build();
+ }
+
+ @Override
+ public CoreContainer getCoreContainer() {
+ return null;
+ }
+
+ @Override
+ public SolrClient newSolrClient(String collection) {
+ String urlScheme = adminClient.getClusterStateProvider().getUrlScheme();
+ var urls =
+ adminClient.getClusterStateProvider().getLiveNodes().stream()
+ .map(liveNode -> URLUtil.getBaseUrlForNodeName(liveNode, urlScheme))
+ .toList();
+ return new CloudSolrClient.Builder(urls).withDefaultCollection(collection).build();
+ }
+
+ @Override
+ public SolrClient getSolrClient() {
+ return adminClient;
+ }
+
+ @Override
+ public void uploadConfigSet(Path configDir, String name) throws SolrServerException, IOException {
+ Path tempZip = Files.createTempFile("configset-", ".zip");
+ try {
+ zipDirectory(configDir, tempZip);
+ new ConfigSetAdminRequest.Upload()
+ .setConfigSetName(name)
+ .setUploadFile(tempZip, "application/zip")
+ .process(adminClient);
+ } finally {
+ Files.deleteIfExists(tempZip);
+ }
+ }
+
+ @Override
+ public boolean hasConfigSet(String name) throws SolrServerException, IOException {
+ return new ConfigSetAdminRequest.List().process(adminClient).getConfigSets().contains(name);
+ }
+
+ /** Packages {@code sourceDir} contents into a zip file. */
+ private static void zipDirectory(Path sourceDir, Path targetZip) throws IOException {
+ try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(targetZip))) {
+ Files.walk(sourceDir)
+ .filter(Files::isRegularFile)
+ .forEach(
+ file -> {
+ try {
+ zos.putNextEntry(new ZipEntry(sourceDir.relativize(file).toString()));
+ Files.copy(file, zos);
+ zos.closeEntry();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void createCollection(CollectionAdminRequest.Create create)
+ throws SolrServerException, IOException {
+ create.process(adminClient);
+ }
+
+ @Override
+ public boolean hasCollection(String name) throws SolrServerException, IOException {
+ return CollectionAdminRequest.listCollections(adminClient).contains(name);
+ }
+
+ @Override
+ public void close() {
+ IOUtils.closeQuietly(adminClient);
+ }
+}
diff --git a/solr/test-framework/src/java/org/apache/solr/embedded/EmbeddedSolrBackend.java b/solr/test-framework/src/java/org/apache/solr/embedded/EmbeddedSolrBackend.java
new file mode 100644
index 00000000000..6b70539afcf
--- /dev/null
+++ b/solr/test-framework/src/java/org/apache/solr/embedded/EmbeddedSolrBackend.java
@@ -0,0 +1,100 @@
+/*
+ * 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.embedded;
+
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import org.apache.solr.SolrBackend;
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.util.IOUtils;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.CoreDescriptor;
+
+/**
+ * {@link SolrBackend} backed by an in-process {@link CoreContainer}/{@link EmbeddedSolrServer}. No
+ * network or ZooKeeper overhead, although there's some request/response serialization.
+ *
+ * <p>Data is persisted in the given {@code solrHome} directory.
+ */
+public class EmbeddedSolrBackend implements SolrBackend {
+
+ private final CoreContainer coreContainer;
+ private final EmbeddedSolrServer adminClient;
+
+ public EmbeddedSolrBackend(Path solrHome) {
+ coreContainer = new CoreContainer(solrHome, new Properties());
+ coreContainer.load();
+ adminClient = new EmbeddedSolrServer(coreContainer, null);
+ }
+
+ /**
+ * @lucene.internal
+ */
+ public EmbeddedSolrBackend(EmbeddedSolrServer solrServer) {
+ this.coreContainer = solrServer.getCoreContainer();
+ this.adminClient = solrServer;
+ }
+
+ @Override
+ public CoreContainer getCoreContainer() {
+ return coreContainer;
+ }
+
+ @Override
+ public SolrClient newSolrClient(String collection) {
+ return new EmbeddedSolrServer(coreContainer, collection);
+ }
+
+ @Override
+ public EmbeddedSolrServer getSolrClient() {
+ return adminClient;
+ }
+
+ @Override
+ public void createCollection(CollectionAdminRequest.Create create) {
+ // convert configSet & properties to a map of "create params":
+ Map<String, String> coreParams = new HashMap<>();
+ if (create.getConfigName() != null) { // actually the configSet name; "config" is ambiguous
+ coreParams.put(CoreDescriptor.CORE_CONFIGSET, create.getConfigName());
+ }
+ if (create.getProperties() != null) {
+ create.getProperties().forEach((k, v) -> coreParams.put(k.toString(), v.toString()));
+ }
+ coreContainer.create(create.getCollectionName(), coreParams);
+ }
+
+ @Override
+ public boolean hasCollection(String name) {
+ return coreContainer.getCoreDescriptor(name) != null;
+ }
+
+ @Override
+ public void reloadCollection(String name) throws SolrException {
+ coreContainer.reload(name);
+ }
+
+ @Override
+ public void close() {
+ IOUtils.closeQuietly(adminClient);
+ coreContainer.shutdown();
+ }
+}
diff --git a/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java b/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java
index 43261fa7fd9..5ecabf8abf9 100644
--- a/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java
+++ b/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java
@@ -30,7 +30,6 @@ import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
-import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.invoke.MethodHandles;
import java.net.BindException;
@@ -38,8 +37,6 @@ import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
-import java.nio.file.Files;
-import java.nio.file.Path;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
@@ -52,11 +49,16 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
+import org.apache.solr.SolrBackend;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.apache.HttpSolrClient;
+import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer;
+import org.apache.solr.client.solrj.jetty.HttpJettySolrClient;
import org.apache.solr.client.solrj.jetty.SSLConfig;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.client.solrj.request.CoresApi;
+import org.apache.solr.common.util.IOUtils;
import org.apache.solr.common.util.TimeSource;
import org.apache.solr.common.util.Utils;
import org.apache.solr.core.CoreContainer;
@@ -105,7 +107,7 @@ import org.slf4j.MDC;
*
* @since solr 1.3
*/
-public class JettySolrRunner {
+public class JettySolrRunner implements SolrBackend {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@@ -146,6 +148,8 @@ public class JettySolrRunner {
private String host;
+ private volatile HttpJettySolrClient jettySolrClient;
+
private volatile boolean started = false;
public static class DebugFilter implements Filter {
@@ -499,9 +503,7 @@ public class JettySolrRunner {
return (RateLimitFilter) rateLimitFilter.getFilter();
}
- /**
- * @return the {@link CoreContainer} for this node
- */
+ @Override
public CoreContainer getCoreContainer() {
SolrServlet servlet;
try {
@@ -590,6 +592,9 @@ public class JettySolrRunner {
setProtocolAndHost();
+ IOUtils.closeQuietly(jettySolrClient);
+ jettySolrClient = null;
+
if (enableProxy) {
if (started) {
proxy.reopen();
@@ -678,6 +683,9 @@ public class JettySolrRunner {
Map<String, String> prevContext = MDC.getCopyOfContextMap();
MDC.clear();
try {
+ IOUtils.closeQuietly(jettySolrClient);
+ jettySolrClient = null;
+
QueuedThreadPool qtp = (QueuedThreadPool) server.getThreadPool();
ReservedThreadExecutor rte = qtp.getBean(ReservedThreadExecutor.class);
@@ -731,29 +739,20 @@ public class JettySolrRunner {
}
}
- public void outputMetrics(Path outputDirectory, String fileName) throws IOException {
+ public void outputMetrics(PrintStream out) throws IOException {
if (getCoreContainer() != null) {
-
- if (outputDirectory != null) {
- Path outDir = outputDirectory;
- Files.createDirectories(outDir);
- }
-
SolrMetricManager metricsManager = getCoreContainer().getMetricManager();
Set<String> registryNames = metricsManager.registryNames();
for (String registryName : registryNames) {
var prometheusReader = metricsManager.getPrometheusMetricReader(registryName);
if (prometheusReader != null) {
- try (OutputStream os =
- outputDirectory == null
- ? OutputStream.nullOutputStream()
- : Files.newOutputStream(outputDirectory.resolve(registryName + "_" + fileName))) {
- new PrometheusTextFormatWriter(false).write(os, prometheusReader.collect());
- }
+ out.println();
+ out.println("# Registry: " + registryName);
+ out.println();
+ new PrometheusTextFormatWriter(false).write(out, prometheusReader.collect());
}
}
-
} else {
throw new IllegalStateException("No CoreContainer found");
}
@@ -919,4 +918,59 @@ public class JettySolrRunner {
public SocketProxy getProxy() {
return proxy;
}
+
+ // ---- SolrBackend implementation ----
+
+ @Override
+ public SolrClient newSolrClient(String collection) {
+ return new HttpJettySolrClient.Builder(getBaseUrl().toString())
+ .withDefaultCollection(collection)
+ .build();
+ }
+
+ @Override
+ public synchronized HttpJettySolrClient getSolrClient() {
+ if (jettySolrClient == null) {
+ jettySolrClient = new HttpJettySolrClient.Builder(getBaseUrl().toString()).build();
+ }
+ return jettySolrClient;
+ }
+
+ private EmbeddedSolrBackend getEmbeddedSolrBackend() {
+ var container = getCoreContainer();
+ if (container.isZooKeeperAware()) {
+ throw new IllegalStateException(
+ "Don't call SolrBackend methods in SolrCloud on JettySolrRunner");
+ }
+ return new EmbeddedSolrBackend(new EmbeddedSolrServer(container, null)); // cheap
+ }
+
+ @Override
+ public void createCollection(CollectionAdminRequest.Create create) {
+ getEmbeddedSolrBackend().createCollection(create);
+ }
+
+ @Override
+ public boolean hasCollection(String name) {
+ return getEmbeddedSolrBackend().hasCollection(name);
+ }
+
+ @Override
+ public void reloadCollection(String name) throws SolrServerException, IOException {
+ getEmbeddedSolrBackend().reloadCollection(name);
+ }
+
+ @Override
+ public String getBaseUrl(Random r) {
+ return getBaseUrl().toString();
+ }
+
+ @Override
+ public void close() {
+ try {
+ stop();
+ } catch (Exception e) {
+ log.error(e.toString(), e); // nowarn
+ }
+ }
}