blob: 5e0461f524048d3346cf9e4c9563d07bbf149c86 [file] [log] [blame]
/*
* 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 static org.apache.solr.core.ConfigSetProperties.DEFAULT_FILENAME;
import com.codahale.metrics.MetricRegistry;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.invoke.MethodHandles;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import java.util.SortedMap;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import javax.servlet.Filter;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.client.solrj.embedded.SSLConfig;
import org.apache.solr.client.solrj.impl.CloudLegacySolrClient;
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.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.cloud.Aliases;
import org.apache.solr.common.cloud.ClusterProperties;
import org.apache.solr.common.cloud.CollectionStatePredicate;
import org.apache.solr.common.cloud.DocCollection;
import org.apache.solr.common.cloud.Replica;
import org.apache.solr.common.cloud.Slice;
import org.apache.solr.common.cloud.SolrZkClient;
import org.apache.solr.common.cloud.ZkMaintenanceUtils;
import org.apache.solr.common.cloud.ZkStateReader;
import org.apache.solr.common.params.CollectionAdminParams;
import org.apache.solr.common.util.ExecutorUtil;
import org.apache.solr.common.util.IOUtils;
import org.apache.solr.common.util.SolrNamedThreadFactory;
import org.apache.solr.common.util.TimeSource;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.core.TracerConfigurator;
import org.apache.solr.embedded.JettyConfig;
import org.apache.solr.embedded.JettySolrRunner;
import org.apache.solr.util.TimeOut;
import org.apache.solr.util.tracing.SimplePropagator;
import org.apache.solr.util.tracing.TraceUtils;
import org.apache.zookeeper.KeeperException;
import org.eclipse.jetty.server.handler.HandlerWrapper;
import org.eclipse.jetty.servlet.ServletHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** "Mini" SolrCloud cluster to be used for testing */
public class MiniSolrCloudCluster {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final URL PRE_GENERATED_PRIVATE_KEY_URL =
MiniSolrCloudCluster.class.getClassLoader().getResource("cryptokeys/priv_key512_pkcs8.pem");
private static final URL PRE_GENERATED_PUBLIC_KEY_URL =
MiniSolrCloudCluster.class.getClassLoader().getResource("cryptokeys/pub_key512.der");
public static final String TEST_URL_ALLOW_LIST = SolrTestCaseJ4.TEST_URL_ALLOW_LIST;
public static final int DEFAULT_TIMEOUT = 30;
public static final String DEFAULT_CLOUD_SOLR_XML =
"<solr>\n"
+ "\n"
+ " <str name=\"shareSchema\">${shareSchema:false}</str>\n"
+ " <str name=\"allowPaths\">${solr.allowPaths:}</str>\n"
+ " <str name=\"configSetBaseDir\">${configSetBaseDir:configsets}</str>\n"
+ " <str name=\"coreRootDirectory\">${coreRootDirectory:.}</str>\n"
+ " <str name=\"collectionsHandler\">${collectionsHandler:solr.CollectionsHandler}</str>\n"
+ " <str name=\"allowUrls\">${"
+ TEST_URL_ALLOW_LIST
+ ":}</str>\n"
+ "\n"
+ " <shardHandlerFactory name=\"shardHandlerFactory\" class=\"HttpShardHandlerFactory\">\n"
+ " <str name=\"urlScheme\">${urlScheme:}</str>\n"
+ " <int name=\"socketTimeout\">${socketTimeout:90000}</int>\n"
+ " <int name=\"connTimeout\">${connTimeout:15000}</int>\n"
+ " </shardHandlerFactory>\n"
+ "\n"
+ " <solrcloud>\n"
+ " <str name=\"host\">127.0.0.1</str>\n"
+ " <int name=\"hostPort\">${hostPort:8983}</int>\n"
+ " <int name=\"zkClientTimeout\">${solr.zkclienttimeout:30000}</int>\n"
+ " <bool name=\"genericCoreNodeNames\">${genericCoreNodeNames:true}</bool>\n"
+ " <int name=\"leaderVoteWait\">${leaderVoteWait:10000}</int>\n"
+ " <int name=\"distribUpdateConnTimeout\">${distribUpdateConnTimeout:45000}</int>\n"
+ " <int name=\"distribUpdateSoTimeout\">${distribUpdateSoTimeout:340000}</int>\n"
+ " <str name=\"zkCredentialsInjector\">${zkCredentialsInjector:org.apache.solr.common.cloud.DefaultZkCredentialsInjector}</str> \n"
+ " <str name=\"zkCredentialsProvider\">${zkCredentialsProvider:org.apache.solr.common.cloud.DefaultZkCredentialsProvider}</str> \n"
+ " <str name=\"zkACLProvider\">${zkACLProvider:org.apache.solr.common.cloud.DefaultZkACLProvider}</str> \n"
+ " <str name=\"pkiHandlerPrivateKeyPath\">${pkiHandlerPrivateKeyPath:"
+ (PRE_GENERATED_PRIVATE_KEY_URL != null
? PRE_GENERATED_PRIVATE_KEY_URL.toExternalForm()
: "")
+ "}</str> \n"
+ " <str name=\"pkiHandlerPublicKeyPath\">${pkiHandlerPublicKeyPath:"
+ (PRE_GENERATED_PUBLIC_KEY_URL != null
? PRE_GENERATED_PUBLIC_KEY_URL.toExternalForm()
: "")
+ "}</str> \n"
+ " <str name=\"distributedClusterStateUpdates\">${solr.distributedClusterStateUpdates:false}</str> \n"
+ " <str name=\"distributedCollectionConfigSetExecution\">${solr.distributedCollectionConfigSetExecution:false}</str> \n"
+ " </solrcloud>\n"
+
// NOTE: this turns off the metrics collection unless overridden by a sysprop
" <metrics enabled=\"${metricsEnabled:false}\">\n"
+ " <reporter name=\"default\" class=\"org.apache.solr.metrics.reporters.SolrJmxReporter\">\n"
+ " <str name=\"rootName\">solr_${hostPort:8983}</str>\n"
+ " </reporter>\n"
+ " </metrics>\n"
+ " \n"
+ "</solr>\n";
private final Object startupWait = new Object();
private volatile ZkTestServer zkServer; // non-final due to injectChaos()
private final boolean externalZkServer;
private final List<JettySolrRunner> jettys = new CopyOnWriteArrayList<>();
private final Path baseDir;
private CloudSolrClient solrClient;
private final JettyConfig jettyConfig;
private final String solrXml;
private final boolean trackJettyMetrics;
private final AtomicInteger nodeIds = new AtomicInteger();
private final Map<String, CloudSolrClient> solrClientByCollection = new ConcurrentHashMap<>();
/**
* Create a MiniSolrCloudCluster with default solr.xml
*
* @param numServers number of Solr servers to start
* @param baseDir base directory that the mini cluster should be run from
* @param jettyConfig Jetty configuration
* @throws Exception if there was an error starting the cluster
*/
public MiniSolrCloudCluster(int numServers, Path baseDir, JettyConfig jettyConfig)
throws Exception {
this(numServers, baseDir, DEFAULT_CLOUD_SOLR_XML, jettyConfig, null, false);
}
/**
* Create a MiniSolrCloudCluster
*
* @param numServers number of Solr servers to start
* @param baseDir base directory that the mini cluster should be run from
* @param solrXml solr.xml file to be uploaded to ZooKeeper
* @param jettyConfig Jetty configuration
* @throws Exception if there was an error starting the cluster
*/
public MiniSolrCloudCluster(int numServers, Path baseDir, String solrXml, JettyConfig jettyConfig)
throws Exception {
this(numServers, baseDir, solrXml, jettyConfig, null, false);
}
/**
* Create a MiniSolrCloudCluster
*
* @param numServers number of Solr servers to start
* @param baseDir base directory that the mini cluster should be run from
* @param solrXml solr.xml file to be uploaded to ZooKeeper
* @param jettyConfig Jetty configuration
* @param zkTestServer ZkTestServer to use. If null, one will be created
* @throws Exception if there was an error starting the cluster
*/
public MiniSolrCloudCluster(
int numServers,
Path baseDir,
String solrXml,
JettyConfig jettyConfig,
ZkTestServer zkTestServer,
boolean formatZkServer)
throws Exception {
this(numServers, baseDir, solrXml, jettyConfig, zkTestServer, Optional.empty(), formatZkServer);
}
/**
* Create a MiniSolrCloudCluster. Note - this constructor visibility is changed to package
* protected so as to discourage its usage. Ideally *new* functionality should use {@linkplain
* SolrCloudTestCase} to configure any additional parameters.
*
* @param numServers number of Solr servers to start
* @param baseDir base directory that the mini cluster should be run from
* @param solrXml solr.xml file to be uploaded to ZooKeeper
* @param jettyConfig Jetty configuration
* @param zkTestServer ZkTestServer to use. If null, one will be created
* @param securityJson A string representation of security.json file (optional).
* @throws Exception if there was an error starting the cluster
*/
MiniSolrCloudCluster(
int numServers,
Path baseDir,
String solrXml,
JettyConfig jettyConfig,
ZkTestServer zkTestServer,
Optional<String> securityJson,
boolean formatZkServer)
throws Exception {
this(
numServers,
baseDir,
solrXml,
jettyConfig,
zkTestServer,
securityJson,
false,
formatZkServer);
}
/**
* Create a MiniSolrCloudCluster. Note - this constructor visibility is changed to package
* protected so as to discourage its usage. Ideally *new* functionality should use {@linkplain
* SolrCloudTestCase} to configure any additional parameters.
*
* @param numServers number of Solr servers to start
* @param baseDir base directory that the mini cluster should be run from
* @param solrXml solr.xml file to be uploaded to ZooKeeper
* @param jettyConfig Jetty configuration
* @param zkTestServer ZkTestServer to use. If null, one will be created
* @param securityJson A string representation of security.json file (optional).
* @param trackJettyMetrics supply jetties with metrics registry
* @throws Exception if there was an error starting the cluster
*/
MiniSolrCloudCluster(
int numServers,
Path baseDir,
String solrXml,
JettyConfig jettyConfig,
ZkTestServer zkTestServer,
Optional<String> securityJson,
boolean trackJettyMetrics,
boolean formatZkServer)
throws Exception {
Objects.requireNonNull(securityJson);
this.baseDir = Objects.requireNonNull(baseDir);
this.jettyConfig = Objects.requireNonNull(jettyConfig);
this.solrXml = solrXml == null ? DEFAULT_CLOUD_SOLR_XML : solrXml;
this.trackJettyMetrics = trackJettyMetrics;
log.info("Starting cluster of {} servers in {}", numServers, baseDir);
Files.createDirectories(baseDir);
this.externalZkServer = zkTestServer != null;
if (!externalZkServer) {
Path zkDir = baseDir.resolve("zookeeper/server1/data");
zkTestServer = new ZkTestServer(zkDir);
try {
zkTestServer.run(formatZkServer);
} catch (Exception e) {
log.error("Error starting Zk Test Server, trying again ...");
zkTestServer.shutdown();
zkTestServer = new ZkTestServer(zkDir);
zkTestServer.run();
}
}
this.zkServer = zkTestServer;
try (SolrZkClient zkClient =
new SolrZkClient.Builder()
.withUrl(zkServer.getZkHost())
.withTimeout(AbstractZkTestCase.TIMEOUT, TimeUnit.MILLISECONDS)
.build()) {
if (!zkClient.exists("/solr/initialized", true)) {
zkClient.makePath("/solr/initialized", "yes".getBytes(Charset.defaultCharset()), true);
if (jettyConfig.sslConfig != null && jettyConfig.sslConfig.isSSLMode()) {
zkClient.makePath(
"/solr" + ZkStateReader.CLUSTER_PROPS,
"{'urlScheme':'https'}".getBytes(StandardCharsets.UTF_8),
true);
}
if (securityJson.isPresent()) { // configure Solr security
zkClient.makePath(
"/solr/security.json", securityJson.get().getBytes(Charset.defaultCharset()), true);
}
}
}
List<Callable<JettySolrRunner>> startups = new ArrayList<>(numServers);
for (int i = 0; i < numServers; ++i) {
startups.add(() -> startJettySolrRunner(newNodeName(), jettyConfig, solrXml));
}
final ExecutorService executorLauncher =
ExecutorUtil.newMDCAwareCachedThreadPool(new SolrNamedThreadFactory("jetty-launcher"));
Collection<Future<JettySolrRunner>> futures = executorLauncher.invokeAll(startups);
ExecutorUtil.shutdownAndAwaitTermination(executorLauncher);
Exception startupError = checkForExceptions("Error starting up MiniSolrCloudCluster", futures);
if (startupError != null) {
try {
this.shutdown();
} catch (Throwable t) {
startupError.addSuppressed(t);
}
throw startupError;
}
solrClient = buildSolrClient();
if (numServers > 0) {
waitForAllNodes(numServers, 60);
}
}
private void waitForAllNodes(int numServers, int timeoutSeconds)
throws InterruptedException, TimeoutException {
log.info("waitForAllNodes: numServers={}", numServers);
int numRunning;
if (timeoutSeconds == 0) {
timeoutSeconds = DEFAULT_TIMEOUT;
}
TimeOut timeout = new TimeOut(timeoutSeconds, TimeUnit.SECONDS, TimeSource.NANO_TIME);
synchronized (startupWait) {
while (numServers != (numRunning = numRunningJetty(getJettySolrRunners()))) {
if (timeout.hasTimedOut()) {
throw new IllegalStateException(
"giving up waiting for all jetty instances to be running. numServers="
+ numServers
+ " numRunning="
+ numRunning);
}
startupWait.wait(500);
}
}
for (JettySolrRunner runner : getJettySolrRunners()) {
waitForNode(runner, (int) timeout.timeLeft(TimeUnit.SECONDS));
}
}
private int numRunningJetty(List<JettySolrRunner> runners) {
int numRunning = 0;
for (JettySolrRunner jsr : runners) {
if (jsr.isRunning()) numRunning++;
}
return numRunning;
}
public void waitForNode(JettySolrRunner jetty, int timeoutSeconds)
throws InterruptedException, TimeoutException {
String nodeName = jetty.getNodeName();
if (nodeName == null) {
throw new IllegalArgumentException("Cannot wait for Jetty with null node name");
}
log.info("waitForNode: {}", nodeName);
getZkStateReader()
.waitForLiveNodes(
timeoutSeconds, TimeUnit.SECONDS, (o, n) -> n != null && n.contains(nodeName));
}
/**
* This method wait till all Solr JVMs ( Jettys ) are running . It waits up to the timeout (in
* seconds) for the JVMs to be up before throwing IllegalStateException. This is called
* automatically on cluster startup and so is only needed when starting additional Jetty
* instances.
*
* @param timeout number of seconds to wait before throwing an IllegalStateException
* @throws IOException if there was an error communicating with ZooKeeper
* @throws InterruptedException if the calling thread is interrupted during the wait operation
* @throws TimeoutException on timeout before all nodes being ready
*/
public void waitForAllNodes(int timeout)
throws IOException, InterruptedException, TimeoutException {
waitForAllNodes(jettys.size(), timeout);
}
private String newNodeName() {
return "node" + nodeIds.incrementAndGet();
}
private Path createInstancePath(String name) throws IOException {
Path instancePath = baseDir.resolve(name);
Files.createDirectory(instancePath);
return instancePath;
}
/**
* @return ZooKeeper server used by the MiniCluster
*/
public ZkTestServer getZkServer() {
return zkServer;
}
/** The {@link ZkStateReader} inside {@link #getSolrClient()}. */
public ZkStateReader getZkStateReader() {
return ZkStateReader.from(getSolrClient());
}
/**
* @return Unmodifiable list of all the currently started Solr Jettys.
*/
public List<JettySolrRunner> getJettySolrRunners() {
return Collections.unmodifiableList(jettys);
}
/**
* @return a randomly-selected Jetty
*/
public JettySolrRunner getRandomJetty(Random random) {
int index = random.nextInt(jettys.size());
return jettys.get(index);
}
/**
* Start a new Solr instance
*
* @param extraServlets Extra servlets to be started by Jetty
* @param extraRequestFilters extra filters to be started by Jetty
* @param sslConfig SSL configuration
* @return new Solr instance
*/
public JettySolrRunner startJettySolrRunner(
String name,
SortedMap<ServletHolder, String> extraServlets,
SortedMap<Class<? extends Filter>, String> extraRequestFilters,
SSLConfig sslConfig)
throws Exception {
return startJettySolrRunner(
name,
JettyConfig.builder()
.withServlets(extraServlets)
.withFilters(extraRequestFilters)
.withSSLConfig(sslConfig)
.build(),
null);
}
public JettySolrRunner getJettySolrRunner(int index) {
return jettys.get(index);
}
/**
* Start a new Solr instance on a particular servlet context
*
* @param name the instance name
* @param config a JettyConfig for the instance's {@link org.apache.solr.embedded.JettySolrRunner}
* @param solrXml the string content of the solr.xml file to use, or null to just use the
* cluster's default
* @return a JettySolrRunner
*/
public JettySolrRunner startJettySolrRunner(String name, JettyConfig config, String solrXml)
throws Exception {
final Properties nodeProps = new Properties();
nodeProps.setProperty("zkHost", zkServer.getZkAddress());
Path runnerPath = createInstancePath(name);
if (solrXml == null) {
solrXml = this.solrXml;
}
Files.write(runnerPath.resolve("solr.xml"), solrXml.getBytes(StandardCharsets.UTF_8));
JettyConfig newConfig = JettyConfig.builder(config).build();
JettySolrRunner jetty =
!trackJettyMetrics
? new JettySolrRunner(runnerPath.toString(), nodeProps, newConfig)
: new JettySolrRunnerWithMetrics(runnerPath.toString(), nodeProps, newConfig);
jetty.start();
jettys.add(jetty);
synchronized (startupWait) {
startupWait.notifyAll();
}
return jetty;
}
/**
* Start a new Solr instance, using the default config
*
* @return a JettySolrRunner
*/
public JettySolrRunner startJettySolrRunner() throws Exception {
return startJettySolrRunner(newNodeName(), jettyConfig, null);
}
/**
* Stop a Solr instance
*
* @param index the index of node in collection returned by {@link #getJettySolrRunners()}
* @return the shut down node
*/
public JettySolrRunner stopJettySolrRunner(int index) throws Exception {
JettySolrRunner jetty = jettys.get(index);
jetty.stop();
jettys.remove(index);
return jetty;
}
/**
* Add a previously stopped node back to the cluster
*
* @param jetty a {@link JettySolrRunner} previously returned by {@link #stopJettySolrRunner(int)}
* @return the started node
* @throws Exception on error
*/
public JettySolrRunner startJettySolrRunner(JettySolrRunner jetty) throws Exception {
jetty.start(false);
if (!jettys.contains(jetty)) jettys.add(jetty);
return jetty;
}
/**
* Stop the given Solr instance. It will be removed from the cluster's list of running instances.
*
* @param jetty a {@link JettySolrRunner} to be stopped
* @return the same {@link JettySolrRunner} instance provided to this method
* @throws Exception on error
*/
public JettySolrRunner stopJettySolrRunner(JettySolrRunner jetty) throws Exception {
jetty.stop();
jettys.remove(jetty);
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()) {
ZkMaintenanceUtils.uploadToZK(
zkClient,
configDir,
ZkMaintenanceUtils.CONFIGS_ZKNODE + "/" + configName,
ZkMaintenanceUtils.UPLOAD_FILENAME_EXCLUDE_PATTERN);
}
}
/** Delete all collections (and aliases) */
public void deleteAllCollections() throws Exception {
try (ZkStateReader reader = new ZkStateReader(getZkClient())) {
final CountDownLatch latch = new CountDownLatch(1);
reader.registerCloudCollectionsListener(
(oldCollections, newCollections) -> {
if (newCollections != null && newCollections.size() == 0) {
latch.countDown();
}
});
reader.createClusterStateWatchersAndUpdate(); // up to date aliases & collections
reader.aliasesManager.applyModificationAndExportToZk(aliases -> Aliases.EMPTY);
for (String collection : reader.getClusterState().getCollectionStates().keySet()) {
CollectionAdminRequest.deleteCollection(collection).process(solrClient);
}
boolean success = latch.await(60, TimeUnit.SECONDS);
if (!success) {
throw new IllegalStateException(
"Still waiting to see all collections removed from clusterstate.");
}
for (String collection : reader.getClusterState().getCollectionStates().keySet()) {
reader.waitForState(collection, 15, TimeUnit.SECONDS, Objects::isNull);
}
}
// may be deleted, but may not be gone yet - we only wait to not see it in ZK, not for core
// unloads
TimeOut timeout = new TimeOut(30, TimeUnit.SECONDS, TimeSource.NANO_TIME);
while (true) {
if (timeout.hasTimedOut()) {
throw new TimeoutException("Timed out waiting for all collections to be fully removed.");
}
boolean allContainersEmpty = true;
for (JettySolrRunner jetty : jettys) {
CoreContainer cc = jetty.getCoreContainer();
if (cc != null && cc.getCores().size() != 0) {
allContainersEmpty = false;
}
}
if (allContainersEmpty) {
break;
}
}
}
public void deleteAllConfigSets() throws Exception {
List<String> configSetNames =
new ConfigSetAdminRequest.List().process(solrClient).getConfigSets();
for (String configSet : configSetNames) {
if (configSet.equals("_default")) {
continue;
}
try {
// cleanup any property before removing the configset
getZkClient()
.delete(
ZkConfigSetService.CONFIGS_ZKNODE + "/" + configSet + "/" + DEFAULT_FILENAME,
-1,
true);
} catch (KeeperException.NoNodeException nne) {
}
new ConfigSetAdminRequest.Delete().setConfigSetName(configSet).process(solrClient);
}
}
/** Shut down the cluster, including all Solr nodes and ZooKeeper */
public void shutdown() throws Exception {
try {
IOUtils.closeQuietly(solrClient);
solrClientByCollection.values().parallelStream()
.forEach(
c -> {
IOUtils.closeQuietly(c);
});
solrClientByCollection.clear();
List<Callable<JettySolrRunner>> shutdowns = new ArrayList<>(jettys.size());
for (final JettySolrRunner jetty : jettys) {
shutdowns.add(() -> stopJettySolrRunner(jetty));
}
jettys.clear();
final ExecutorService executorCloser =
ExecutorUtil.newMDCAwareCachedThreadPool(new SolrNamedThreadFactory("jetty-closer"));
Collection<Future<JettySolrRunner>> futures = executorCloser.invokeAll(shutdowns);
ExecutorUtil.shutdownAndAwaitTermination(executorCloser);
Exception shutdownError =
checkForExceptions("Error shutting down MiniSolrCloudCluster", futures);
if (shutdownError != null) {
throw shutdownError;
}
} finally {
if (!externalZkServer) {
zkServer.shutdown();
}
resetRecordingFlag();
}
}
public Path getBaseDir() {
return baseDir;
}
public CloudSolrClient getSolrClient() {
return solrClient;
}
/**
* Returns a SolrClient that has a defaultCollection set for it. SolrClients are cached by their
* collectionName for reuse and are closed for you.
*
* @param collectionName The name of the collection to get a SolrClient for.
* @return CloudSolrClient configured for the specific collection.
*/
public CloudSolrClient getSolrClient(String collectionName) {
return solrClientByCollection.computeIfAbsent(
collectionName,
k -> {
CloudSolrClient solrClient =
new CloudLegacySolrClient.Builder(
Collections.singletonList(zkServer.getZkAddress()), Optional.empty())
.withDefaultCollection(collectionName)
.withSocketTimeout(90000)
.withConnectionTimeout(15000)
.build();
solrClient.connect();
if (log.isInfoEnabled()) {
log.info(
"Created solrClient for collection {} with updatesToLeaders={} and parallelUpdates={}",
collectionName,
solrClient.isUpdatesToLeaders(),
solrClient.isParallelUpdates());
}
return solrClient;
});
}
public SolrZkClient getZkClient() {
return getZkStateReader().getZkClient();
}
/**
* Set data in zk without exposing caller to the ZK API, i.e. tests won't need to include
* Zookeeper dependencies
*/
public void zkSetData(String path, byte[] data, boolean retryOnConnLoss)
throws InterruptedException {
try {
getZkClient().setData(path, data, -1, retryOnConnLoss);
} catch (KeeperException e) {
throw new SolrException(ErrorCode.UNKNOWN, "Failed writing to Zookeeper", e);
}
}
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);
boolean ok = true;
for (Future<JettySolrRunner> future : futures) {
try {
future.get();
} catch (ExecutionException e) {
parsed.addSuppressed(e.getCause());
ok = false;
} catch (InterruptedException e) {
Thread.interrupted();
throw e;
}
}
return ok ? null : parsed;
}
/** Return the jetty that a particular replica resides on */
public JettySolrRunner getReplicaJetty(Replica replica) {
for (JettySolrRunner jetty : jettys) {
if (jetty.isStopped()) continue;
if (replica.getCoreUrl().startsWith(jetty.getBaseUrl().toString())) return jetty;
}
throw new IllegalArgumentException(
"Cannot find Jetty for a replica with core url " + replica.getCoreUrl());
}
/** Make the zookeeper session on a particular jetty lose connection and expire */
public void expireZkSession(JettySolrRunner jetty) {
CoreContainer cores = jetty.getCoreContainer();
if (cores != null) {
ChaosMonkey.causeConnectionLoss(jetty);
zkServer.expire(cores.getZkController().getZkClient().getZooKeeper().getSessionId());
if (log.isInfoEnabled()) {
log.info("Expired zookeeper session from node {}", jetty.getBaseUrl());
}
}
}
// Currently not used ;-(
public synchronized void injectChaos(Random random) throws Exception {
// sometimes we restart one of the jetty nodes
if (random.nextBoolean()) {
JettySolrRunner jetty = jettys.get(random.nextInt(jettys.size()));
jetty.stop();
log.info("============ Restarting jetty");
jetty.start();
}
// sometimes we restart zookeeper
if (random.nextBoolean()) {
zkServer.shutdown();
log.info("============ Restarting zookeeper");
zkServer = new ZkTestServer(zkServer.getZkDir(), zkServer.getPort());
zkServer.run(false);
}
// sometimes we cause a connection loss - sometimes it will hit the overseer
if (random.nextBoolean()) {
JettySolrRunner jetty = jettys.get(random.nextInt(jettys.size()));
ChaosMonkey.causeConnectionLoss(jetty);
}
}
public Overseer getOpenOverseer() {
List<Overseer> overseers = new ArrayList<>();
for (int i = 0; i < jettys.size(); i++) {
JettySolrRunner runner = getJettySolrRunner(i);
if (runner.getCoreContainer() != null) {
overseers.add(runner.getCoreContainer().getZkController().getOverseer());
}
}
return getOpenOverseer(overseers);
}
public static Overseer getOpenOverseer(List<Overseer> overseers) {
ArrayList<Overseer> shuffledOverseers = new ArrayList<Overseer>(overseers);
Collections.shuffle(shuffledOverseers, LuceneTestCase.random());
for (Overseer overseer : shuffledOverseers) {
if (!overseer.isClosed()) {
return overseer;
}
}
throw new SolrException(ErrorCode.NOT_FOUND, "No open Overseer found");
}
public void waitForActiveCollection(
String collection, long wait, TimeUnit unit, int shards, int totalReplicas) {
log.info("waitForActiveCollection: {}", collection);
CollectionStatePredicate predicate = expectedShardsAndActiveReplicas(shards, totalReplicas);
AtomicReference<DocCollection> state = new AtomicReference<>();
AtomicReference<Set<String>> liveNodesLastSeen = new AtomicReference<>();
try {
getZkStateReader()
.waitForState(
collection,
wait,
unit,
(n, c) -> {
state.set(c);
liveNodesLastSeen.set(n);
return predicate.matches(n, c);
});
} catch (TimeoutException | InterruptedException e) {
throw new RuntimeException(
"Failed while waiting for active collection"
+ "\n"
+ e.getMessage()
+ "\nLive Nodes: "
+ Arrays.toString(liveNodesLastSeen.get().toArray())
+ "\nLast available state: "
+ state.get());
}
}
public void waitForActiveCollection(String collection, int shards, int totalReplicas) {
waitForActiveCollection(collection, 30, TimeUnit.SECONDS, shards, totalReplicas);
}
public void waitForActiveCollection(String collection, long wait, TimeUnit unit) {
log.info("waitForActiveCollection: {}", collection);
CollectionStatePredicate predicate = expectedActive();
AtomicReference<DocCollection> state = new AtomicReference<>();
AtomicReference<Set<String>> liveNodesLastSeen = new AtomicReference<>();
try {
getZkStateReader()
.waitForState(
collection,
wait,
unit,
(n, c) -> {
state.set(c);
liveNodesLastSeen.set(n);
return predicate.matches(n, c);
});
} catch (TimeoutException | InterruptedException e) {
throw new RuntimeException(
"Failed while waiting for active collection"
+ "\n"
+ e.getMessage()
+ "\nLive Nodes: "
+ Arrays.toString(liveNodesLastSeen.get().toArray())
+ "\nLast available state: "
+ state.get());
}
}
public static CollectionStatePredicate expectedShardsAndActiveReplicas(
int expectedShards, int expectedReplicas) {
return (liveNodes, collectionState) -> {
if (collectionState == null) return false;
if (collectionState.getSlices().size() != expectedShards) {
return false;
}
int activeReplicas = 0;
for (Slice slice : collectionState) {
for (Replica replica : slice) {
if (replica.isActive(liveNodes)) {
activeReplicas++;
}
}
}
return activeReplicas == expectedReplicas;
};
}
public static CollectionStatePredicate expectedActive() {
return (liveNodes, collectionState) -> {
if (collectionState == null) return false;
for (Slice slice : collectionState) {
for (Replica replica : slice) {
if (!replica.isActive(liveNodes)) {
return false;
}
}
}
return true;
};
}
public void waitForJettyToStop(JettySolrRunner runner) throws TimeoutException {
String nodeName = runner.getNodeName();
if (nodeName == null) {
log.info("Cannot wait for Jetty with null node name");
return;
}
log.info("waitForJettyToStop: {}", nodeName);
try {
getZkStateReader().waitForLiveNodes(15, TimeUnit.SECONDS, (o, n) -> !n.contains(nodeName));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new SolrException(ErrorCode.SERVER_ERROR, "interrupted", e);
}
}
public void dumpMetrics(File outputDirectory, String fileName) throws IOException {
for (JettySolrRunner jetty : jettys) {
jetty.outputMetrics(outputDirectory, fileName);
}
}
public void dumpCoreInfo(PrintStream pw) throws IOException {
for (JettySolrRunner jetty : jettys) {
jetty.dumpCoresInfo(pw);
}
}
/**
* @lucene.experimental
*/
public static final class JettySolrRunnerWithMetrics extends JettySolrRunner {
public JettySolrRunnerWithMetrics(String solrHome, Properties nodeProps, JettyConfig config) {
super(solrHome, nodeProps, config);
}
private volatile MetricRegistry metricRegistry;
@Override
protected HandlerWrapper injectJettyHandlers(HandlerWrapper chain) {
metricRegistry = new MetricRegistry();
io.dropwizard.metrics.jetty10.InstrumentedHandler metrics =
new io.dropwizard.metrics.jetty10.InstrumentedHandler(metricRegistry);
metrics.setHandler(chain);
return metrics;
}
/**
* @return optional subj. It may be null, if it's not yet created.
*/
public MetricRegistry getMetricRegistry() {
return metricRegistry;
}
}
private static class Config {
final String name;
final Path path;
private Config(String name, Path path) {
this.name = name;
this.path = path;
}
}
/** Builder class for a MiniSolrCloudCluster */
public static class Builder {
private final int nodeCount;
private final Path baseDir;
private String solrXml = DEFAULT_CLOUD_SOLR_XML;
private JettyConfig.Builder jettyConfigBuilder;
private Optional<String> securityJson = Optional.empty();
private List<Config> configs = new ArrayList<>();
private Map<String, Object> clusterProperties = new HashMap<>();
private boolean trackJettyMetrics;
private boolean useDistributedCollectionConfigSetExecution;
private boolean useDistributedClusterStateUpdate;
private boolean formatZkServer = true;
private boolean disableTraceIdGeneration = false;
/**
* Create a builder
*
* @param nodeCount the number of nodes in the cluster
* @param baseDir a base directory for the cluster
*/
public Builder(int nodeCount, Path baseDir) {
this.nodeCount = nodeCount;
this.baseDir = baseDir;
jettyConfigBuilder = JettyConfig.builder();
}
/** Use a JettyConfig.Builder to configure the cluster's jetty servers */
public Builder withJettyConfig(Consumer<JettyConfig.Builder> fun) {
fun.accept(jettyConfigBuilder);
return this;
}
/** Use the provided string as solr.xml content */
public Builder withSolrXml(String solrXml) {
this.solrXml = solrXml;
return this;
}
/** Read solr.xml from the provided path */
public Builder withSolrXml(Path solrXml) {
try {
this.solrXml = new String(Files.readAllBytes(solrXml), Charset.defaultCharset());
} catch (IOException e) {
throw new RuntimeException(e);
}
return this;
}
/**
* Configure the specified security.json for the {@linkplain MiniSolrCloudCluster}
*
* @param securityJson The path specifying the security.json file
* @return the instance of {@linkplain Builder}
*/
public Builder withSecurityJson(Path securityJson) {
try {
this.securityJson =
Optional.of(new String(Files.readAllBytes(securityJson), Charset.defaultCharset()));
} catch (IOException e) {
throw new RuntimeException(e);
}
return this;
}
/**
* Configure the specified security.json for the {@linkplain MiniSolrCloudCluster}
*
* @param securityJson The string specifying the security.json configuration
* @return the instance of {@linkplain Builder}
*/
public Builder withSecurityJson(String securityJson) {
this.securityJson = Optional.of(securityJson);
return this;
}
/**
* Upload a collection config before tests start
*
* @param configName the config name
* @param configPath the path to the config files
*/
public Builder addConfig(String configName, Path configPath) {
this.configs.add(new Config(configName, configPath));
return this;
}
/**
* This method makes the MiniSolrCloudCluster use the "other" Collection API execution strategy
* than it normally would. When some test classes call this method (and some don't) we make sure
* that a run of multiple tests with a single seed will exercise both code lines (distributed
* and Overseer based Collection API) so regressions can be spotted faster.
*
* <p>The real need is for a few tests covering reasonable use cases to call this method. If
* you're adding a new test, you don't have to call it (but it's ok if you do).
*/
public Builder useOtherCollectionConfigSetExecution() {
// Switch from Overseer to distributed Collection execution and vice versa
useDistributedCollectionConfigSetExecution = !useDistributedCollectionConfigSetExecution;
// Reverse distributed cluster state updates as well if possible (state can't be Overseer
// based if Collections API is distributed)
useDistributedClusterStateUpdate =
!useDistributedClusterStateUpdate || useDistributedCollectionConfigSetExecution;
return this;
}
/**
* Force the cluster Collection and config state API execution as well as the cluster state
* update strategy to be either Overseer based or distributed. <b>This method can be useful when
* debugging tests</b> failing in only one of the two modes to have all local runs exhibit the
* issue, as well obviously for tests that are not compatible with one of the two modes.
*
* <p>If this method is not called, the strategy being used will be random if the configuration
* passed to the cluster ({@code solr.xml} equivalent) contains a placeholder similar to:
*
* <pre>{@code
* <solrcloud>
* ....
* <str name="distributedClusterStateUpdates">${solr.distributedClusterStateUpdates:false}</str>
* <str name="distributedCollectionConfigSetExecution">${solr.distributedCollectionConfigSetExecution:false}</str>
* ....
* </solrcloud>
* }</pre>
*
* For an example of a configuration supporting this setting, see {@link
* MiniSolrCloudCluster#DEFAULT_CLOUD_SOLR_XML}. When a test sets a different {@code solr.xml}
* config (using {@link #withSolrXml}), if the config does not contain the placeholder, the
* strategy will be defined by the values assigned to {@code useDistributedClusterStateUpdates}
* and {@code useDistributedCollectionConfigSetExecution} in {@link
* org.apache.solr.core.CloudConfig.CloudConfigBuilder}.
*
* @param distributedCollectionConfigSetApi When {@code true}, Collection and Config Set API
* commands are executed in a distributed way by nodes. When {@code false}, they are
* executed by Overseer.
* @param distributedClusterStateUpdates When {@code true}, cluster state updates are handled in
* a distributed way by nodes. When {@code false}, cluster state updates are handled by
* Overseer.
* <p>If {@code distributedCollectionConfigSetApi} is {@code true} then this parameter must
* be {@code true}.
*/
@SuppressWarnings("InvalidParam")
public Builder withDistributedClusterStateUpdates(
boolean distributedCollectionConfigSetApi, boolean distributedClusterStateUpdates) {
useDistributedCollectionConfigSetExecution = distributedCollectionConfigSetApi;
useDistributedClusterStateUpdate = distributedClusterStateUpdates;
return this;
}
/**
* Set a cluster property
*
* @param propertyName the property name
* @param propertyValue the property value
*/
public Builder withProperty(String propertyName, String propertyValue) {
this.clusterProperties.put(propertyName, propertyValue);
return this;
}
public Builder withMetrics(boolean trackJettyMetrics) {
this.trackJettyMetrics = trackJettyMetrics;
return this;
}
public Builder formatZkServer(boolean formatZkServer) {
this.formatZkServer = formatZkServer;
return this;
}
/**
* Configure and run the {@link MiniSolrCloudCluster}
*
* @throws Exception if an error occurs on startup
*/
public MiniSolrCloudCluster configure() throws Exception {
return SolrCloudTestCase.cluster = build();
}
/**
* Configure, run and return the {@link MiniSolrCloudCluster}
*
* @throws Exception if an error occurs on startup
*/
public MiniSolrCloudCluster build() throws Exception {
// Two lines below will have an impact on how the MiniSolrCloudCluster and therefore the test
// run if the config being
// used in the test does have the appropriate placeholders. See for example
// DEFAULT_CLOUD_SOLR_XML in MiniSolrCloudCluster.
// Hard coding values here will impact such tests.
// To hard code behavior for tests not having these placeholders - and for SolrCloud as well
// for that matter! -
// change the values assigned to useDistributedClusterStateUpdates and
// useDistributedCollectionConfigSetExecution in
// org.apache.solr.core.CloudConfig.CloudConfigBuilder. Do not forget then to revert before
// commit!
System.setProperty(
"solr.distributedCollectionConfigSetExecution",
Boolean.toString(useDistributedCollectionConfigSetExecution));
System.setProperty(
"solr.distributedClusterStateUpdates",
Boolean.toString(useDistributedClusterStateUpdate));
// eager init to prevent OTEL init races caused by test setup
if (!disableTraceIdGeneration && TracerConfigurator.TRACE_ID_GEN_ENABLED) {
SimplePropagator.load();
injectRandomRecordingFlag();
}
JettyConfig jettyConfig = jettyConfigBuilder.build();
MiniSolrCloudCluster cluster =
new MiniSolrCloudCluster(
nodeCount,
baseDir,
solrXml,
jettyConfig,
null,
securityJson,
trackJettyMetrics,
formatZkServer);
for (Config config : configs) {
cluster.uploadConfigSet(config.path, config.name);
}
if (clusterProperties.size() > 0) {
ClusterProperties props = new ClusterProperties(cluster.getZkClient());
for (Map.Entry<String, Object> entry : clusterProperties.entrySet()) {
props.setClusterProperty(entry.getKey(), entry.getValue());
}
}
return cluster;
}
public Builder withDefaultClusterProperty(String key, String value) {
@SuppressWarnings({"unchecked"})
HashMap<String, Object> defaults =
(HashMap<String, Object>) this.clusterProperties.get(CollectionAdminParams.DEFAULTS);
if (defaults == null) {
defaults = new HashMap<>();
this.clusterProperties.put(CollectionAdminParams.DEFAULTS, defaults);
}
@SuppressWarnings({"unchecked"})
HashMap<String, Object> cluster =
(HashMap<String, Object>) defaults.get(CollectionAdminParams.CLUSTER);
if (cluster == null) {
cluster = new HashMap<>();
defaults.put(CollectionAdminParams.CLUSTER, cluster);
}
cluster.put(key, value);
return this;
}
/**
* Disables the default/built-in simple trace ID generation/propagation.
*
* <p>Tracers are registered as global singletons and if for example a test needs to use a
* MockTracer or a "real" Tracer, it needs to call this method so that the test setup doesn't
* accidentally reset the Tracer it wants to use.
*/
public Builder withTraceIdGenerationDisabled() {
this.disableTraceIdGeneration = true;
return this;
}
}
/**
* Randomizes the tracing Span::isRecording check.
*
* <p>This will randomize the Span::isRecording check so we have better coverage of all methods
* that deal with span creation without having to enable otel module.
*
* <p>It only makes sense to call this if we are using the alwaysOn tracer, the OTEL tracer
* already has this flag turned on and randomizing it would risk not recording trace data.
*
* <p>Note. Tracing is not a SolrCloud only feature. this method is placed here for convenience
* only, any test can make use of this example for more complete coverage of the tracing
* mechanics.
*/
private static void injectRandomRecordingFlag() {
try {
boolean isRecording = LuceneTestCase.rarely();
TraceUtils.IS_RECORDING = (ignored) -> isRecording;
} catch (IllegalStateException e) {
// This can happen in benchmarks or other places that aren't in a randomized test
log.warn("Unable to inject random recording flag due to outside randomized context", e);
}
}
private static void resetRecordingFlag() {
TraceUtils.IS_RECORDING = TraceUtils.DEFAULT_IS_RECORDING;
}
}